From 7e8b2292f8fb6414439742fa4b1f442548b2b28a Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 12:34:37 +0100 Subject: [PATCH 01/30] drm/apple: fix the merged firmware-27 table's A428 method and try it on T8142 iomfb_v27.c named A428 dcpep_pipe_set_428, which this tree calls iomfbep_set_brightness_correction (94621d9edb9e). T8142 with firmware-compat 27.0.0 takes DCP_FIRMWARE_V_27_0: the H17P interface makes the 27.0 DCP firmware panic after the first hotplug. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 24cc8c18c0809..1047e385f7952 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -5001,6 +5001,18 @@ static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) char fw_str[DCP_FW_VERSION_STR_LEN]; int ret; + /* + * 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. + */ + 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")) + return DCP_FIRMWARE_V_27_0; + } + if (hw->firmware_compat == DCP_FIRMWARE_V_27_0) { ret = dcp_read_fw_version(dev, "apple,firmware-compat", compat_str); if (ret < 0 || strcmp(compat_str, "27.0.0")) From 121d6e9782653477e277005543d330d1bc04deb6 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 12:45:10 +0100 Subject: [PATCH 02/30] drm/apple: T8142: run the macOS 27.0 DCP firmware as firmware 27 On the 27.0 stub the J813's DCP firmware (AppleDCP-1317.1.1~116) panics right after the first hotplug when driven through the H17P interface ("ap_link: callback failed with: 0x5" in its crash log), so apple-drm never binds and the unwind WARNs in devm_clk_match. Drive it as the T6050's firmware 27 instead, for a T8142 whose firmware-compat is 27.0.0: - DCP_FIRMWARE_V_27_0 with the default method profile: with H17G every swap fails with status 4; - apple,clock-ids = <585 431> (/arm-io/disp0 clock-ids) with two fixed clocks, named firmware-0 and firmware-1: firmware 27 asks for its provider clocks by index. The DCP takes those by name, so clkref stays the first clock and the 26.x paths keep the clock they had. The binding gets a T8142 branch for this layout. The J813 ADT's clock-frequencies (ids 0x100..0x1ad) has no rate for 585 or 431. Firmware 27 reads only 585, as its minimum pixel clock (getClockFrequency(0)). With 0 it logs "getClockFrequency(0) (0) < videoClock 138521008! Giving up on frequencies". 585 gets 712 MHz, clock-frequencies entry 0x49, which its low byte names; with it macOS 27.0's DCP firmware offered a display under aura. The firmware never asked for 431 in these boots, which keeps 0. Result: DCP booted, 2560x1600@60, fb0, sway, no swap failures, no WARNING. The H17G profile would also have selected the J813's alpha semantics and powerlog-service matching ("failed to extract init props"). Signed-off-by: Thomas Glanzmann --- .../bindings/display/apple,dcp.yaml | 28 ++++++++++++++- .../boot/dts/apple/t8142-j813-display.dtsi | 34 ++++++++++++++++++- drivers/gpu/drm/apple/dcp.c | 31 +++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index 6a494d59a7f89..6d1c092f2509d 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -482,7 +482,13 @@ allOf: - const: 27 - const: 0 - const: 0 - else: + - if: + not: + properties: + compatible: + contains: + enum: ['apple,t6050-dcp', 'apple,t6050-dcpext', 'apple,t8142-dcp'] + then: properties: clocks: maxItems: 2 @@ -492,6 +498,26 @@ allOf: - const: clock-194 apple,clock-ids: false apple,firmware-physical-regs: false + # T8142 runs both 26.x and 27.0 DCP firmware. The 26.x paths use the first + # clock; firmware 27 asks for the provider clocks of apple,clock-ids by + # index, as firmware-0 and firmware-1. + - if: + properties: + compatible: + contains: + const: apple,t8142-dcp + then: + properties: + clocks: + minItems: 1 + maxItems: 3 + clock-names: + minItems: 1 + items: + - const: clock-154 + - const: firmware-0 + - const: firmware-1 + apple,firmware-physical-regs: false - if: not: properties: diff --git a/arch/arm64/boot/dts/apple/t8142-j813-display.dtsi b/arch/arm64/boot/dts/apple/t8142-j813-display.dtsi index d9e890af7fd44..812b558be0ee6 100644 --- a/arch/arm64/boot/dts/apple/t8142-j813-display.dtsi +++ b/arch/arm64/boot/dts/apple/t8142-j813-display.dtsi @@ -79,7 +79,14 @@ apple,bw-doorbell = <&pmgr_dcp 1 6>; /* Scan out the primary plane on IOMFB surface 2. */ apple,iomfb-surfaces = <0 0 1 0>; - clocks = <&clkref>; + /* + * clkref is the clock the 26.x firmware paths use. Firmware 27 + * (DCP_FIRMWARE_V_27_0) asks for its provider clocks by index into + * /arm-io/disp0 clock-ids, in this order: firmware-0 and firmware-1. + */ + apple,clock-ids = <585 431>; + clocks = <&clkref>, <&dcp_fw_clk585>, <&dcp_fw_clk431>; + clock-names = "clock-154", "firmware-0", "firmware-1"; /* generate phandle explicitly for use in loader */ phandle = <&dcp>; status = "disabled"; @@ -106,3 +113,28 @@ }; }; }; + +/ { + /* + * The DCP firmware's clocks 585 and 431 (/arm-io/disp0 clock-ids). The + * J813 ADT's clock-frequencies (ids 0x100..0x1ad) has no entry for + * either. Firmware 27 reads only 585, as its minimum pixel clock + * (getClockFrequency(0)); below its 138.521 MHz video clock it gives up + * on frequencies. 712 MHz is clock-frequencies entry 0x49, the one 585's + * low byte names; with it macOS 27.0's DCP firmware offered a display + * under aura. The firmware never asks for 431, which has no rate here. + */ + dcp_fw_clk585: clock-dcp-firmware-585 { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <712000000>; + clock-output-names = "dcp_firmware_585"; + }; + + dcp_fw_clk431: clock-dcp-firmware-431 { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <0>; + clock-output-names = "dcp_firmware_431"; + }; +}; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 1047e385f7952..663e311333b71 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -5263,6 +5263,30 @@ static int dcp_firmware_metadata_v27(struct apple_dcp *dcp) clocks = 0; if (clocks < 0 || clocks > 64) return -EINVAL; + /* + * A DCP that also boots earlier firmware (T8142) lists the clock those + * use first and names the provider clocks "firmware-". Take only + * the named ones here. + */ + if (clocks && + of_property_match_string(dev->of_node, "clock-names", "firmware-0") >= 0) { + dcp->firmware_clocks = devm_kcalloc(dev, clocks, + sizeof(*dcp->firmware_clocks), + GFP_KERNEL); + if (!dcp->firmware_clocks) + return -ENOMEM; + for (index = 0; index < clocks; index++) { + dcp->firmware_clocks[index].id = + devm_kasprintf(dev, GFP_KERNEL, "firmware-%d", index); + if (!dcp->firmware_clocks[index].id) + return -ENOMEM; + } + ret = devm_clk_bulk_get(dev, clocks, dcp->firmware_clocks); + if (ret) + return ret; + dcp->num_firmware_clocks = clocks; + return 0; + } ret = devm_clk_bulk_get_all(dev, &dcp->firmware_clocks); if (ret < 0) return ret; @@ -6137,6 +6161,13 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) mutex_init(&dcp->dcpavserv.lock); INIT_WORK(&dcp->dcpavserv.close_wq, dcpavserv_close_work); dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev); + /* + * T8142 on firmware 27 runs as the T6050's firmware-27 instance, with + * the default method profile: with H17G every swap fails with status 4. + */ + if (fw_compat == DCP_FIRMWARE_V_27_0 && + of_device_is_compatible(dev->of_node, "apple,t8142-dcp")) + dcp->hw.iomfb_method_profile = DCP_IOMFB_METHODS_DEFAULT; /* The display such a processor finds at boot is its first sink. */ if (dcp_sink_metadata(dcp)) dcp_desktop_metadata_new_sink(dcp); From 43f89808a996ed280b375651a8797f5b4d4c2460 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 20:54:38 +0100 Subject: [PATCH 03/30] drm/apple: T8142 on firmware 27: offer only alpha formats on the plane 5e98b68eb5ec offers the H17G primary plane only alpha formats, since its firmware blends XRGB's X byte as alpha. Since the T8142 runs macOS 27.0's DCP firmware as firmware 27 it is outside the H17G profile, so XRGB8888 came back, and the same blending: GPU-rendered alacritty under sway showed wrong colours. Treat the T8142 on firmware 27 the same way. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/apple_drv.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 38d31dd0e110b..87c43cab1fffc 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -1135,11 +1135,23 @@ static int apple_probe_per_dcp(struct device *dev, interchange = supports_interchange && (zpos <= 1 || surf != top_surf); + /* + * The T8142 runs macOS 27.0's DCP firmware as firmware + * 27, outside the H17G profile, and blends XRGB's X + * byte as alpha all the same (GPU-rendered alacritty + * showed wrong colours on the panel). + */ + bool xrgb_is_alpha = + dcp_data->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17G || + (dcp_data->fw_compat == DCP_FIRMWARE_V_27_0 && + of_device_is_compatible(dcp_data->dev->of_node, + "apple,t8142-dcp")); + planes[zpos] = apple_plane_init(drm, 1U << num, surf, supports_l10r, supports_xrgb2101010, - dcp_data->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17G, + xrgb_is_alpha, plane_type, interchange, supports_premultiplied); } From d89d34777bab5d0a30448276482466982843b6ed Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 20:55:36 +0100 Subject: [PATCH 04/30] drm/apple: say why a present is dropped at the backlight step iomfb_apply_backlight_h17p() returning false makes dcp_swap_submit() drop the present, which then reports only "swap failed! status 4294967295". On the J813 (firmware 27) that happened while typing in alacritty under sway. Name the cause: no transaction, no brightness maximum, or the backlight prepare's error. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/iomfb.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index c15f77c0aaa7c..c652e6444df2b 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -3283,13 +3283,21 @@ bool iomfb_apply_backlight_h17p(struct apple_dcp *dcp, dcp->brightness.maximum, false); return true; } - if (!transaction || !dcp->brightness.maximum) + if (!transaction || !dcp->brightness.maximum) { + /* The caller drops this present ("swap failed"): say why. */ + dev_warn_ratelimited(dcp->dev, + "present dropped: transaction %d brightness maximum %u\n", + !!transaction, dcp->brightness.maximum); return false; + } for (i = 0; i < SWAP_SURFACES; i++) have_surface |= !request->surf_null[i]; ret = dcp_backlight_prepare(dcp, have_surface, &present); - if (ret && ret != -EALREADY) + if (ret && ret != -EALREADY) { + dev_warn_ratelimited(dcp->dev, + "present dropped: backlight prepare %d\n", ret); return false; + } if (!ret) { transaction->backlight = present; transaction->backlight_reserved = true; From db0c05a868dd65ff0625f49fdcf79b22395e3813 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 12:56:44 +0100 Subject: [PATCH 05/30] drm/asahi: T8142 on 26A428: probe the G17 path and log the GPU identity Without aura's 25G76 firmware profile (the macOS 27.0 stub), the T8142 probe takes the G17 path with a bring-up configuration that refuses at the identity check, after get_g17_id has logged the ID registers. On J813: family 0xb, variant 2, one die, one cluster of ten core slots, core mask 0x3bd. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/driver.rs | 11 ++++++ drivers/gpu/drm/asahi/hw/mod.rs | 1 + drivers/gpu/drm/asahi/hw/t8142.rs | 61 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 drivers/gpu/drm/asahi/hw/t8142.rs diff --git a/drivers/gpu/drm/asahi/driver.rs b/drivers/gpu/drm/asahi/driver.rs index 2b8bdac35f43d..1a7a5138bbcea 100644 --- a/drivers/gpu/drm/asahi/driver.rs +++ b/drivers/gpu/drm/asahi/driver.rs @@ -547,6 +547,17 @@ impl AsahiPlatformDriver { fn probe_m5<'bound>( pdev: &'bound platform::Device>, ) -> Result> { + // Without aura's 25G76 profile (the macOS 27.0 stub's 26A428 firmware), take the M5 + // graph's G17 path. J813 bring-up: it logs the GPU's identity and refuses. + let has_25g76 = of::chosen().is_some_and(|chosen| { + matches!( + chosen.get_opt_property::>(c"aurora,m5-gpu-firmware-profile"), + Ok(Some(_)) + ) + }); + if !has_25g76 { + return Self::probe_g17(pdev, regs::G17Resources::new(pdev)?, &hw::t8142::PROBE); + } // SAFETY: The M5 UAT has 42 output bits. unsafe { pdev.dma_set_mask_and_coherent(DmaMask::try_new(42)?)? }; let resources = regs::Resources::new(pdev)?; diff --git a/drivers/gpu/drm/asahi/hw/mod.rs b/drivers/gpu/drm/asahi/hw/mod.rs index 94f8f11e81bb4..a58fac5e12c37 100644 --- a/drivers/gpu/drm/asahi/hw/mod.rs +++ b/drivers/gpu/drm/asahi/hw/mod.rs @@ -21,6 +21,7 @@ pub(crate) mod t8103; pub(crate) mod t8112; pub(crate) mod t8140; pub(crate) mod t6050; +pub(crate) mod t8142; #[cfg(CONFIG_DRM_ASAHI_G18)] pub(crate) mod t8152; diff --git a/drivers/gpu/drm/asahi/hw/t8142.rs b/drivers/gpu/drm/asahi/hw/t8142.rs new file mode 100644 index 0000000000000..c60e22f7889b3 --- /dev/null +++ b/drivers/gpu/drm/asahi/hw/t8142.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! M5 (T8142, MacBook Air J813) on the macOS 27.0 (26A428) 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 +//! core masks, before the UAT or the firmware are touched. +//! +//! J813 (2026-11-07): ID registers 0xb022000 0xb0800 0x11010a 0x40404 0x1320311 0x10030302, +//! family 0xb, variant 2, one die, one cluster of ten core slots; core mask 0x3bd (eight +//! enabled, slots 1 and 6 fused off). +use crate::g17::{Config, CoreLayout, CrashBuffers, FirmwareProfile, FixedBuffer, Identity}; +use crate::hw::{GpuGen, GpuVariant}; + +// The ADT sgx gfx-data-shared-ro-base and gfx1-data-shared-ro-base (each 0x8000) under 27.0, +// where the M5 Pro's coprocessors ask for their crash logs. +const J813_CRASH_BUFFERS: [FixedBuffer; 2] = [ + FixedBuffer { va: 0x100_0250_4000, size: 0x8000 }, + FixedBuffer { va: 0x100_025c_0000, size: 0x8000 }, +]; + +/// Refuses the GPU after logging its identity: no variant byte is 0xff. +pub(crate) const PROBE: Config = Config { + chip_id: 0x8142, + identity: Identity { + family: 0xb, + variant: 0xff, + num_dies: 1, + gpu_variant: GpuVariant::G, + usc_generation: 3, + hal_generation: 300, + }, + uat_ias: 42, + uat_oas: 42, + // As on 25G76: each role publishes its own kernel root, the secondary 0x40000 into + // `pagetables`. + secondary_root_offset: Some(0x40000), + crash_buffers: CrashBuffers::Fixed(J813_CRASH_BUFFERS), + profile: FirmwareProfile::M5PublishedRoots, + pm_metrics: None, + windows: &[], + reserved_slots: &[], + perf: None, + opps: 0, + uat_quirks: crate::mmu::UatQuirks::DEFAULT, + region_c: None, + chip: None, + // Never reached: the identity check refuses first. + m5: Some(&crate::g17::fw::m5::initdata::J714), + adopt_root_entries: true, + fabric_bridge: false, + recovery_slot_keys: None, + gpu_generation: GpuGen::G17, + firmware: crate::g17::fw::variant::Firmware::Fixed(&crate::g17::fw::variant::V26A428_M5), + core_layout: CoreLayout { + cores: None, + alternate_core_masks: None, + operand_state: crate::g17::fw::compute::OperandState::TWO_BY_TEN, + deflake: crate::g17::fw::render::Deflake::TWO_CLUSTERS, + compute_blocks: crate::g17::fw::freelist::COMPUTE_BLOCKS, + }, +}; From 482acb1862d8c1d6911aadf1498e71bfde32762e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 13:17:38 +0100 Subject: [PATCH 06/30] drm/asahi: g17: M5: the J813 (T8142) on the 26A428 graph (bring-up) A J813 board for the M5 graph and a T8142 configuration on the G17 path, for the macOS 27.0 (26A428) GPU firmware: - Board: chip ID 0x8142, one cluster of ten core slots, core type 0x20 revision 5 and twelve states (its 25G76 HW data), the ADT's perf-states (padded to fourteen with the top state), no CS/AFR table, the RT0g AON row, J714's slots at the J813's addresses (slot 17 widened to 0x28000: the firmware reads G + 0x25148). - Its firmware checks root +0 against 0x0c8b_4339_83a0_04c0 (TEXT 0x21ed8), the common 26A428 interface, and has no M5 HW data insert: Board::interface_version and Board::hw_data_insert, with the later HW data offsets shifted back. - Crash logs in the firmware's own DATA_SHARED_RO (FirmwareData) and a trial fw-data reservation in the J813 DT. - OperandState::ONE_BY_TEN. Both roles now acknowledge their InitData. Signed-off-by: Thomas Glanzmann --- arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi | 16 +- drivers/gpu/drm/asahi/driver.rs | 4 +- drivers/gpu/drm/asahi/g17/fw/compute.rs | 11 ++ drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 185 ++++++++++++++++-- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 8 +- drivers/gpu/drm/asahi/hw/t8142.rs | 40 +++- 6 files changed, 229 insertions(+), 35 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi b/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi index d1b8b540538bd..763125bfe4ec7 100644 --- a/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi +++ b/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi @@ -22,6 +22,17 @@ reg = <0 0 0 0>; }; + /* + * macOS 27.0 (26A428): both roles' __DATA and __DATA_SHARED_RO, contiguous + * (ADT gfx-asc and gfx1-asc segment-ranges), where the firmware places its crash + * logs. Fixed at the addresses iBoot has used on every 27.0 boot so far, until the + * bootloader fills it in. + */ + /* Mapped like gfx-data: the driver reads the crash logs through the linear map. */ + gpu_fw_data: fw-data { + reg = <0x100 0x02444000 0x0 0x184000>; + }; + uat_handoff: uat-handoff { reg = <0 0 0 0>; }; @@ -93,9 +104,10 @@ mboxes = <&gfx_mbox>, <&gfx1_mbox>; power-domains = <&ps_gfx>; memory-region = <&uat_ttbs>, <&uat_pagetables>, <&uat_handoff>, - <&uat_l2_pagetables>, <&gfx_data>, <&gfx1_data>; + <&uat_l2_pagetables>, <&gfx_data>, <&gfx1_data>, + <&gpu_fw_data>; memory-region-names = "ttbs", "pagetables", "handoff", - "shared-l2", "gfx-data", "gfx1-data"; + "shared-l2", "gfx-data", "gfx1-data", "fw-data"; operating-points-v2 = <&gpu_opp>; /* Holds the DCS and fabric floor while GPU work is in flight. */ apple,clpc = <&clpc>; diff --git a/drivers/gpu/drm/asahi/driver.rs b/drivers/gpu/drm/asahi/driver.rs index 1a7a5138bbcea..ba6301fa1c595 100644 --- a/drivers/gpu/drm/asahi/driver.rs +++ b/drivers/gpu/drm/asahi/driver.rs @@ -548,7 +548,7 @@ impl AsahiPlatformDriver { pdev: &'bound platform::Device>, ) -> Result> { // Without aura's 25G76 profile (the macOS 27.0 stub's 26A428 firmware), take the M5 - // graph's G17 path. J813 bring-up: it logs the GPU's identity and refuses. + // graph's G17 path (J813 bring-up). let has_25g76 = of::chosen().is_some_and(|chosen| { matches!( chosen.get_opt_property::>(c"aurora,m5-gpu-firmware-profile"), @@ -556,7 +556,7 @@ impl AsahiPlatformDriver { ) }); if !has_25g76 { - return Self::probe_g17(pdev, regs::G17Resources::new(pdev)?, &hw::t8142::PROBE); + return Self::probe_g17(pdev, regs::G17Resources::new(pdev)?, &hw::t8142::CONFIG); } // SAFETY: The M5 UAT has 42 output bits. unsafe { pdev.dma_set_mask_and_coherent(DmaMask::try_new(42)?)? }; diff --git a/drivers/gpu/drm/asahi/g17/fw/compute.rs b/drivers/gpu/drm/asahi/g17/fw/compute.rs index 76f4f96bd6ea6..94d8f44edc7bc 100644 --- a/drivers/gpu/drm/asahi/g17/fw/compute.rs +++ b/drivers/gpu/drm/asahi/g17/fw/compute.rs @@ -200,6 +200,17 @@ impl OperandState { r0a5c9: 0x9000, size: 0x14000, }; + /// One cluster of ten cores (M5, J813), by ONE_BY_SIX's and TWO_BY_TEN's pattern: a bank of + /// ten slots of 0x1800, one tail of 0x800, the end rounded up to 16 KiB. + pub(crate) const ONE_BY_TEN: OperandState = OperandState { + r10229: 0x1e800, + r140a8: 0x1f000, + r10099: 0xf405, + r10091: 0x1e400, + r0a5c1: 0x0005, + r0a5c9: 0xf000, + size: 0x20000, + }; /// Two clusters of ten cores (M4 Pro): bank stride 0x1e400, tails 0x1000. pub(crate) const TWO_BY_TEN: OperandState = OperandState { r10229: 0x3c800, diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index ed397b27917cb..b316baa011d44 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -1538,9 +1538,13 @@ fn encode_m5_aux_perf_states(board: &Board, out: &mut [u8]) { let table = &mut out[M5_HW_AUX_PERF_STATE.. M5_HW_AUX_PERF_STATE + M5_HW_AUX_PERF_STATE_SIZE]; table.fill(0); + // Without CS/AFR domains (M5) the table stays zero, as in the J813's 25G76 HW data. + let Some(aux_perf_states) = board.aux_perf_states else { + return; + }; put_u32(table, 0, 13); put_u32(table, 4, 14); - for (state, &(frequency, core, sram)) in board.aux_perf_states.iter().enumerate() { + for (state, &(frequency, core, sram)) in aux_perf_states.iter().enumerate() { put_u32(table, 8 + state * 4, frequency); for lane in 0..2 { put_u32(table, 0x48 + state * 8 + lane * 4, core[lane]); @@ -1612,6 +1616,9 @@ const J714_AON_RECORDS: [[u32; 3]; 3] = [ [0x7230_5452, 0x00ce_0001, 0x0000_1401], ]; +// J813's row tagged RT0g (one GPU cluster), as its 25G76 AON block carries. +const J813_AON_RECORDS: [[u32; 3]; 1] = [[0x6730_5452, 0x001d_0001, 0x0000_1401]]; + // J716c's rows tagged RT0g, RT1g and RT0r. const J716C_AON_RECORDS: [[u32; 3]; 3] = [ [0x6730_5452, 0x0058_0001, 0x0000_1401], @@ -1675,7 +1682,8 @@ pub(crate) const M5_HW_DATA_SIZE: usize = ABI_ALLOCATION_SIZES_26A428_M5[ALLOC_H pub(crate) const M5_KICK_SHARED_SIZE: usize = 0x2000; pub(crate) const M5_KICK_SHARED_POINTER: usize = 0x27dc; -pub(crate) fn set_m5_kick_shared(out: &mut [u8], address: u64) -> Result<(), InitdataError> { +pub(crate) fn set_m5_kick_shared(board: &Board, out: &mut [u8], address: u64) + -> Result<(), InitdataError> { if out.len() != M5_HW_DATA_SIZE { return Err(InitdataError::BufferSize); } @@ -1686,7 +1694,7 @@ pub(crate) fn set_m5_kick_shared(out: &mut [u8], address: u64) -> Result<(), Ini if address & (UAT_PAGE_SIZE as u64 - 1) != 0 { return Err(InitdataError::AddressOutOfRange); } - put_u64(out, M5_KICK_SHARED_POINTER, address); + put_u64(out, hw_offset(board, M5_KICK_SHARED_POINTER), address); Ok(()) } @@ -1753,21 +1761,41 @@ pub(crate) const M5_HW_DATA_INSERT: usize = 0x267c; pub(crate) const M5_HW_DATA_INSERT_LEN: usize = 8; +/// The board's offset of a field at `offset` of the M5 Pro's HW data: below the insert they +/// agree, past it a board without the insert has the field eight bytes lower. +const fn hw_offset(board: &Board, offset: usize) -> usize { + if !board.hw_data_insert && offset >= M5_HW_DATA_INSERT + M5_HW_DATA_INSERT_LEN { + offset - M5_HW_DATA_INSERT_LEN + } else { + offset + } +} + /// What the M5 graph's HW data and power policy depend on per board. Every board is a T6050 /// running the same firmware build; [`crate::g17::Config::m5`] names the board's. pub(crate) struct Board { + /// The chip ID the HW data names. + chip_id: u32, + /// The InitData interface version the board's firmware checks at root +0x00, where it + /// differs from the M5 Pro's ([`M5_INITDATA_VERSION_WORD`]). + pub(crate) interface_version: Option, + /// Whether the board's HW data has the M5 Pro firmware's eight bytes at + /// [`M5_HW_DATA_INSERT`]; the common 26A428 interface (J813) has not. + hw_data_insert: bool, /// GPU clusters. pub(crate) clusters: u32, /// Cores of all clusters. pub(crate) gpu_cores: u32, /// The core type and firmware revision the HW data names. core_revision: (u32, u32), - /// Main-domain performance states. + /// Performance states after the off state, the HW data's count. + perf_state_count: u32, + /// Main-domain performance states. A board with fewer than 13 repeats its top state. pub(crate) perf_states: &'static [M5PerfState; 14], /// Voltage tables in [`Board::perf_states`]. perf_tables: usize, - /// Auxiliary-domain performance states. - aux_perf_states: &'static [M5AuxPerfState; 14], + /// Auxiliary-domain (CS/AFR) performance states, where the GPU has those domains. + aux_perf_states: Option<&'static [M5AuxPerfState; 14]>, /// The relative-frequency curve of [`Board::perf_states`]. relative_frequency: [u32; 16], /// Rows of the board's AON table the firmware reads. @@ -1803,12 +1831,16 @@ pub(crate) struct Board { /// M5 Pro: two clusters of ten cores. pub(crate) const J714: Board = Board { + chip_id: 0x6050, + interface_version: None, + hw_data_insert: true, clusters: 2, gpu_cores: 20, core_revision: (0x18, 4), + perf_state_count: J714_GPU_NUM_PERF_STATES, perf_states: &J714_GPU_PERF_STATES, perf_tables: 2, - aux_perf_states: &J714_AUX_PERF_STATES, + aux_perf_states: Some(&J714_AUX_PERF_STATES), relative_frequency: relative_frequency(&J714_GPU_PERF_STATES, 1), aon_records: &J714_AON_RECORDS, register_windows: &J714_REGISTER_WINDOWS, @@ -1826,12 +1858,16 @@ pub(crate) const J714: Board = Board { /// M5 Max: four clusters of ten cores. pub(crate) const J716C: Board = Board { + chip_id: 0x6050, + interface_version: None, + hw_data_insert: true, clusters: 4, gpu_cores: 40, core_revision: (0x19, 4), + perf_state_count: J714_GPU_NUM_PERF_STATES, perf_states: &J716C_GPU_PERF_STATES, perf_tables: 4, - aux_perf_states: &J716C_AUX_PERF_STATES, + aux_perf_states: Some(&J716C_AUX_PERF_STATES), relative_frequency: relative_frequency(&J716C_GPU_PERF_STATES, 1), aon_records: &J716C_AON_RECORDS, register_windows: &J716C_REGISTER_WINDOWS, @@ -1854,14 +1890,18 @@ const J775D_PERF_FLOOR: usize = 3; /// M5 Ultra: two dies of four clusters of ten cores (G17D). Values from the J775D ADT. pub(crate) const J775D: Board = Board { + chip_id: 0x6050, + interface_version: None, + hw_data_insert: true, clusters: 8, gpu_cores: 80, // Firmware revision 2 for raw revision 0x11. core_revision: (0x1a, 2), + perf_state_count: J714_GPU_NUM_PERF_STATES, perf_states: &J775D_GPU_PERF_STATES, // One voltage table per cluster. perf_tables: 8, - aux_perf_states: &J775D_AUX_PERF_STATES, + aux_perf_states: Some(&J775D_AUX_PERF_STATES), relative_frequency: relative_frequency(&J775D_GPU_PERF_STATES, J775D_PERF_FLOOR), aon_records: &J775D_AON_RECORDS, register_windows: &J775D_REGISTER_WINDOWS, @@ -1884,6 +1924,39 @@ pub(crate) const J775D: Board = Board { completion_inputs: false, }; +/// M5 (T8142, MacBook Air J813): one cluster of ten core slots, eight enabled. Values from the +/// J813 ADT (27.0) and its 25G76 HW data (core type 0x20 revision 5, twelve states). +/// Bring-up: no policy controller configuration yet. +pub(crate) const J813: Board = Board { + chip_id: 0x8142, + // The J813's 26A428 firmware compares root +0x00 with 0x0c8b_4339_83a0_04c0 (movz/movk at + // TEXT 0x21ed8 in both roles), the common 26A428 interface J873 also runs, not the M5 Pro's. + interface_version: Some(COMMON26_INITDATA_VERSION_WORD), + // The common 26A428 HW data, without the M5 Pro's insert. + hw_data_insert: false, + clusters: 1, + gpu_cores: 10, + core_revision: (0x20, 5), + perf_state_count: 12, + perf_states: &J813_GPU_PERF_STATES, + perf_tables: 1, + aux_perf_states: None, + relative_frequency: relative_frequency(&J813_GPU_PERF_STATES, 1), + aon_records: &J813_AON_RECORDS, + register_windows: &J813_REGISTER_WINDOWS, + min_parameter_buffer: (super::target::min_parameter_buffer_pages(1) << 15) as u32, + // Half J714's: 2 MiB render blocks in a 2 GiB inventory. + render_pool: (0x20_0000, 1 << 31), + calibration: None, + controller_defaults: None, + idle_standby_us: None, + mcc_mask: None, + topology_words: None, + fan_curve_start_mc: 84_000, + g17d_forms: false, + completion_inputs: false, +}; + const _: () = { use super::target::min_parameter_buffer_pages; assert!(J714.min_parameter_buffer as u64 == min_parameter_buffer_pages(J714.clusters) << 15); @@ -1893,7 +1966,8 @@ const _: () = { /// Populate the enabled-USC count from the device's sampled core masks. /// This count covers the whole device, not one cluster. -pub(crate) fn set_m5_enabled_cores(out: &mut [u8], enabled: u32) -> Result<(), InitdataError> { +pub(crate) fn set_m5_enabled_cores(board: &Board, out: &mut [u8], enabled: u32) + -> Result<(), InitdataError> { if out.len() != M5_HW_DATA_SIZE { return Err(InitdataError::BufferSize); } @@ -1901,7 +1975,7 @@ pub(crate) fn set_m5_enabled_cores(out: &mut [u8], enabled: u32) -> Result<(), I return Err(InitdataError::InterfaceMismatch); } put_u32(out, 0x10a4, enabled); - put_u32(out, 0x268c, enabled); + put_u32(out, hw_offset(board, 0x268c), enabled); Ok(()) } @@ -1953,6 +2027,30 @@ pub(crate) const J714_GPU_PERF_STATES: [M5PerfState; 14] = two_tables([ (1578, 1000, 1000, 1000, 1000), (1620, 1025, 1040, 1025, 1040), ]); +/// One-table states (MHz, core voltage, SRAM voltage), the rest 0. +const fn one_table(states: [(u32, u32, u32); 14]) -> [M5PerfState; 14] { + let mut out = [(0, [0; M5_PERF_LANES], [0; M5_PERF_LANES]); 14]; + let mut state = 0; + while state < 14 { + let (frequency, core, sram) = states[state]; + out[state].0 = frequency; + out[state].1[0] = core; + out[state].2[0] = sram; + state += 1; + } + out +} + +// J813 ADT sgx perf-states and perf-states-sram (MHz, mV): twelve states after the off state. +// The encoder's table has fourteen; the last repeats the top state, which the HW data's count +// of twelve leaves out. +pub(crate) const J813_GPU_PERF_STATES: [M5PerfState; 14] = one_table([ + (0, 125, 770), (338, 660, 770), (486, 675, 770), (636, 690, 770), + (796, 705, 770), (888, 735, 770), (988, 760, 770), (1084, 790, 790), + (1182, 830, 830), (1278, 865, 865), (1374, 890, 890), (1470, 945, 945), + (1578, 1010, 1010), (1578, 1010, 1010), +]); + /// Four-table states (MHz, four core-voltage tables, four SRAM-voltage tables), the rest 0. const fn four_tables(states: [(u32, [u32; 4], [u32; 4]); 14]) -> [M5PerfState; 14] { let mut out = [(0, [0; M5_PERF_LANES], [0; M5_PERF_LANES]); 14]; @@ -2075,6 +2173,47 @@ const J714_REGISTER_WINDOWS: [M5RegisterWindowSpec; 30] = { windows }; +/// J813: J714's 26A428 slots at the J813's addresses, matched through its ADT and its 25G76 +/// windows (M5_25G76_REGISTER_WINDOWS: from the ID registers up, 26A428 numbers each GPU block +/// five slots higher). Slots 4 and 11 sit where J714's do, AIC + 0x68000 and MCC reg 9 + +/// 0x8000. Bring-up: slot 20, per cluster on T6050, is left out. +const J813_GPU_BASE: u64 = 0x5_8000_0000; +const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 29] = { + const G: u64 = J813_GPU_BASE; + [ + jw(0, &[0x3_8101_4000], 0x4000, 0, 2), + jw(3, &[0x2_2010_4000], 0x18000, 0, 2), + jw(4, &[0x3_8106_8000], 0x4000, 0, 0), + jw(9, &[0x3_803d_0000], 0x4000, 0, 2), + jw(10, &[0x3_803c_0000], 0x4000, 0, 0), + jw(11, &[0x3_803d_8000], 0x4000, 0, 2), + jw(12, &[0x4_2165_c000], 0x4000, 0, 2), + jw(14, &[0x3_8028_0000], 0x8000, 0, 0), + jw(15, &[0x3_8c84_0000], 0x24000, 0, 2), + // 25G76 maps 0x21400; the 26A428 firmware reads G + 0x25148 at bootstrap. + jw(17, &[G], 0x28000, 0, 2), + jw(18, &[G + 0xeb_4000], 0x4000, 0, 2), + jw(22, &[G + 0x100_0000], 0x8000, 0, 2), + jw(23, &[G + 0xf0_0000], 0x10000, 0, 2), + jw(24, &[G + 0xec_0000], 0x10000, 0, 2), + jw(31, &[G + 0xd0_4000], 0x8000, 0xd0_4000, 2), + jw(33, &[G + 0xd5_0000], 0x10000, 0xd5_0000, 2), + jw(34, &[G + 0xd1_0000], 0x4000, 0xd1_0000, 2), + jw(36, &[G + 0xd4_0000], 0x4000, 0xd4_0000, 2), + jw(37, &[G + 0xd6_0000], 0x8000, 0xd6_0000, 2), + jw(38, &[G + 0xd4_4000], 0x4000, 0xd4_4000, 2), + jw(39, &[G + 0xd4_c000], 0x200, 0xd4_c000, 2), + jw(40, &[G + 0xe0_0000], 0x4000, 0xe0_0000, 2), + jw(42, &[G + 0xe4_0000], 0x4000, 0xe4_0000, 2), + jw(44, &[G + 0xe0_8000, G + 0xe1_0000], 0x8000, 0, 2), + jw(46, &[G + 0xe3_3800], 0x4000, 0, 2), + jw(48, &[G + 0xe6_0000], 0x58, 0xe6_0000, 2), + jw(52, &[G + 0xe4_8000], 0x8000, 0, 2), + jw(53, &[G + 0x2_3d00], 0x200, 0x2_3d00, 2), + jw(58, &[G + 0xe7_3800], 0x4000, 0, 2), + ] +}; + /// J714 with all eight AMCCs in slot 3 (J716c amcc-enable-mask 0xff, MCC /// reg[14..21] + 0x104000) and four instances, one per cluster, in slots 20 and /// 24, where J714 has two at the same stride. Slot 44 is a fixed pair on every M5 @@ -2271,8 +2410,8 @@ pub(crate) fn encode_m5_hw_data( write_relocated_words(M5_25G76_HW_PLATFORM_WORDS, 0, COMMON26_HW_DATA_BANDS, g18)?; encode_m5_register_map(board, windows, &mut g18[HW_REGISTER_MAP_OFFSET..COMMON26_HW_REGISTER_MAP_END])?; - put_u32(g18, COMMON26_HW_CHIP_ID_OFFSET, 0x6050); - put_u32(g18, COMMON26_HW_PERF_STATE_COUNT, J714_GPU_NUM_PERF_STATES); + put_u32(g18, COMMON26_HW_CHIP_ID_OFFSET, board.chip_id); + put_u32(g18, COMMON26_HW_PERF_STATE_COUNT, board.perf_state_count); put_u32(g18, COMMON26_HW_FIXED_WORD_OFFSET, COMMON26_HW_FIXED_WORD_VALUE); put_u32(g18, 0x0fb4, 1); @@ -2302,20 +2441,24 @@ pub(crate) fn encode_m5_hw_data( for index in 0..COMMON26_HW_PERF_ROWS { put_u32(g18, COMMON26_HW_PERF_INDEX_MAP + index * 4, index as u32); put_u32(g18, COMMON26_HW_PERF_RELATIVE_B + index * 4, board.relative_frequency[index]); - put_u32(g18, M5_HW_AUX_RELATIVE_FREQUENCY + index * 4, board.relative_frequency[index]); + if board.aux_perf_states.is_some() { + put_u32(g18, M5_HW_AUX_RELATIVE_FREQUENCY + index * 4, board.relative_frequency[index]); + } } encode_m5_aux_perf_states(board, g18); let table = COMMON26_KSM_COMPLETION_TABLE_OFFSET; g18[table..table + completion.len()].copy_from_slice(&completion); put_u64(g18, COMMON26_QOS_POINTER_OFFSET, qos_high); - let insert = M5_HW_DATA_INSERT; - let len = M5_HW_DATA_INSERT_LEN; - out.copy_within(insert..COMMON26_HW_DATA_SIZE, insert + len); - out[insert..insert + len].fill(0); + if board.hw_data_insert { + let insert = M5_HW_DATA_INSERT; + let len = M5_HW_DATA_INSERT_LEN; + out.copy_within(insert..COMMON26_HW_DATA_SIZE, insert + len); + out[insert..insert + len].fill(0); + } put_u32(out, 0x2660, 0); - put_u32(out, 0x2728, 0); - put_u32(out, 0x272c, 1); + put_u32(out, hw_offset(board, 0x2728), 0); + put_u32(out, hw_offset(board, 0x272c), 1); if let Some(mask) = board.mcc_mask { put_u64(out, 0x10a8, mask); } @@ -2324,7 +2467,7 @@ pub(crate) fn encode_m5_hw_data( put_u32(out, 0x1048, cores); } // The inherited J813 minimum parameter-buffer size is replaced per board. - put_u32(out, 0x26b0, board.min_parameter_buffer); + put_u32(out, hw_offset(board, 0x26b0), board.min_parameter_buffer); Ok(()) } diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index ec150b54f1bee..2b5d26fe3c951 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -163,13 +163,13 @@ impl InitData { }; m::encode_m5_hw_data(board, completions, QOS, windows, &mut self.object_mut(self.bundle)?.bytes_mut()[..m::M5_HW_DATA_SIZE]).map_err(|_| EINVAL)?; - m::set_m5_kick_shared( + m::set_m5_kick_shared(board, &mut self.object_mut(self.bundle)?.bytes_mut()[..m::M5_HW_DATA_SIZE], KICK_SHARED) .map_err(|_| EINVAL)?; let masked_cores: u32 = id.core_masks.iter().map(|mask| mask.count_ones()).sum(); let enabled_cores = if masked_cores != 0 { masked_cores } else { id.num_clusters.checked_mul(id.num_cores).ok_or(EOVERFLOW)? }; - m::set_m5_enabled_cores( + m::set_m5_enabled_cores(board, &mut self.object_mut(self.bundle)?.bytes_mut()[..m::M5_HW_DATA_SIZE], enabled_cores) .map_err(|_| EINVAL)?; m::encode_m5_bundle_policy(m::M5PolicyBindings { @@ -220,6 +220,10 @@ impl InitData { m::encode_root_for_profile(profile, role, &addresses, &instance, &mut self.object_mut(self.roots)?.bytes_mut()[offset..offset + m::M5_ROOT_SIZE]) .map_err(|_| EINVAL)?; + if let Some(version) = board.interface_version { + self.object_mut(self.roots)?.bytes_mut()[offset..offset + 8] + .copy_from_slice(&version.to_le_bytes()); + } } fence(Ordering::SeqCst); dev_info!(dev.as_ref(), "M5 firmware graph ready: private {:#x}+{:#x}, shared {:#x}+{:#x}\n", diff --git a/drivers/gpu/drm/asahi/hw/t8142.rs b/drivers/gpu/drm/asahi/hw/t8142.rs index c60e22f7889b3..044e28f20684e 100644 --- a/drivers/gpu/drm/asahi/hw/t8142.rs +++ b/drivers/gpu/drm/asahi/hw/t8142.rs @@ -8,15 +8,9 @@ //! J813 (2026-11-07): ID registers 0xb022000 0xb0800 0x11010a 0x40404 0x1320311 0x10030302, //! family 0xb, variant 2, one die, one cluster of ten core slots; core mask 0x3bd (eight //! enabled, slots 1 and 6 fused off). -use crate::g17::{Config, CoreLayout, CrashBuffers, FirmwareProfile, FixedBuffer, Identity}; +use crate::g17::{Config, CoreLayout, CrashBuffers, FirmwareProfile, Identity}; use crate::hw::{GpuGen, GpuVariant}; -// The ADT sgx gfx-data-shared-ro-base and gfx1-data-shared-ro-base (each 0x8000) under 27.0, -// where the M5 Pro's coprocessors ask for their crash logs. -const J813_CRASH_BUFFERS: [FixedBuffer; 2] = [ - FixedBuffer { va: 0x100_0250_4000, size: 0x8000 }, - FixedBuffer { va: 0x100_025c_0000, size: 0x8000 }, -]; /// Refuses the GPU after logging its identity: no variant byte is 0xff. pub(crate) const PROBE: Config = Config { @@ -34,7 +28,9 @@ pub(crate) const PROBE: Config = Config { // As on 25G76: each role publishes its own kernel root, the secondary 0x40000 into // `pagetables`. secondary_root_offset: Some(0x40000), - crash_buffers: CrashBuffers::Fixed(J813_CRASH_BUFFERS), + // Each role asks for its crash log at the physical address of its __DATA_SHARED_RO + // (0x100_0250_4000, 0x100_025c_0000 under 27.0), inside the `fw-data` region. + crash_buffers: CrashBuffers::FirmwareData, profile: FirmwareProfile::M5PublishedRoots, pm_metrics: None, windows: &[], @@ -59,3 +55,31 @@ pub(crate) const PROBE: Config = Config { compute_blocks: crate::g17::fw::freelist::COMPUTE_BLOCKS, }, }; + +/// The J813 on the M5 graph: [`PROBE`] with the GPU's identity and the J813 board. +pub(crate) const CONFIG: Config = Config { + identity: Identity { + family: 0xb, + variant: 2, + num_dies: 1, + gpu_variant: GpuVariant::G, + usc_generation: 3, + hal_generation: 300, + }, + m5: Some(&crate::g17::fw::m5::initdata::J813), + // One cluster of ten core slots; the buffers are sized for all ten. + core_layout: CoreLayout { + cores: None, + alternate_core_masks: None, + operand_state: crate::g17::fw::compute::OperandState::ONE_BY_TEN, + deflake: crate::g17::fw::render::Deflake::ONE_CLUSTER, + compute_blocks: crate::g17::fw::freelist::COMPUTE_BLOCKS, + }, + ..PROBE +}; + +// Each compute operand bank holds 6 KiB per core slot of the board. +const _: () = assert!( + CONFIG.core_layout.operand_state.bank_bytes() + == crate::g17::fw::m5::initdata::J813.gpu_cores * 0x1800 +); From ea2f3091128fefde312c8ac0d4b36a0f393a01a0 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 13:34:11 +0100 Subject: [PATCH 07/30] drm/asahi: g17: M5: J813 InitData order, GFX1 first (bring-up) The J813's macOS publishes and acknowledges GFX1's InitData before GFX's (its 25G76 trace); the M5 Pro's graph starts with the primary. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/mod.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 4e68d0bd74a47..4ee9dd8b3f8f9 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1911,15 +1911,24 @@ impl Gpu { .prepare_m5_firmware() .inspect_err(at("M5 preparation"))?; let ack = shared.variant.init_ack; - session - .primary - .init(session.init.root_va(Role::Primary)?, ack) - .inspect_err(at("primary InitData"))?; - dev_info!(dev.as_ref(), "M5 primary initdata acknowledged\n"); - let root_va = session.init.root_va(Role::Secondary)?; + let primary_root = session.init.root_va(Role::Primary)?; + let secondary_root = session.init.root_va(Role::Secondary)?; + // The J813's macOS publishes and acknowledges GFX1's InitData first, then GFX's (its + // 25G76 trace); the M5 Pro's graph starts with the primary. + if cfg.chip_id == 0x8142 { + let secondary = session.secondary.as_mut().ok_or(EINVAL)?; + secondary.init(secondary_root, ack).inspect_err(at("secondary InitData"))?; + dev_info!(dev.as_ref(), "M5 secondary initdata acknowledged\n"); + session.primary.init(primary_root, ack).inspect_err(at("primary InitData"))?; + dev_info!(dev.as_ref(), "M5 primary initdata acknowledged\n"); + } else { + session.primary.init(primary_root, ack).inspect_err(at("primary InitData"))?; + dev_info!(dev.as_ref(), "M5 primary initdata acknowledged\n"); + let secondary = session.secondary.as_mut().ok_or(EINVAL)?; + secondary.init(secondary_root, ack).inspect_err(at("secondary InitData"))?; + dev_info!(dev.as_ref(), "M5 secondary initdata acknowledged\n"); + } let secondary = session.secondary.as_mut().ok_or(EINVAL)?; - secondary.init(root_va, ack).inspect_err(at("secondary InitData"))?; - dev_info!(dev.as_ref(), "M5 secondary initdata acknowledged\n"); session.primary.notify(MSG_POWER_ON_DONE)?; secondary.notify(MSG_POWER_ON_DONE)?; Self::wait_for_opening(dev, &session.init, &session.primary, session.secondary.as_ref()) From 4b4cfaf480c211c1cdc30c2839eb368332d482f7 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 14:20:44 +0100 Subject: [PATCH 08/30] drm/asahi: g17: M5: the J813 on the common 26A428 InitData layout (bring-up) The J813's 26A428 GPU firmware checks the common 26A428 interface word, the one J873 runs, and the M5 graph gave it the M5 Pro's layout with only that word replaced. The two builds differ well beyond the root: the perf/policy block is 0x7800 bytes, not 0x126b0, with the policy state and AON PTD pointers at +0x3d30 and +0x64d8 (M5 Pro +0x8254, +0x11a1c); the status config is 0x6d0 bytes with its cross-link at +0x6c4; region C keeps the 25G76 fields moved up by four. Everything after the policy block in the shared cluster moves with it. Add a per-board Graph (root profile, both cluster layouts, bundle views, work rings, state grids, the region-C power-assert tally) and give the J813 the common one: the J873 profile for the root, main config and channels, and common 26A428 encoders for the private cluster, the policy blocks and region C. Those carry the J813's own 25G76 words through the J873 backend's relocation bands, plus the fields 26A428 added as macOS sets them on J873; the J813-scaled values come out where J873 has its own (1200 for 1300, 0x5eb0 for 0x8f16). The runtime's layout users follow the board's graph; the M5 Pro-only telemetry is skipped on the common build. Also for the J813: its one memory cache controller (HW data +0x10a8 was J714's 0xf), its own 25G76 word at common +0x1044 (6, J714 12), slot 17 at the common map's 0x36000, DPEP (29), ADCLK (30) and slot 32, the J813's 25G76 slot 27. On the J813 (27.0): both roles ACK InitData; GFX1 no longer crashes (it hit NULL+0x44 before). GFX still takes an SError right after its ACK: its crash log's bus-error section names 0x5_8002_1140/0x5_8002_1148 (KSM, the HW data's completion-queue table in x13), so the KSM block does not answer. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 358 +++++++++++++++++++- drivers/gpu/drm/asahi/g17/initdata.rs | 38 ++- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 46 ++- 3 files changed, 397 insertions(+), 45 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index b316baa011d44..7a62dfb14d487 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -1761,6 +1761,322 @@ pub(crate) const M5_HW_DATA_INSERT: usize = 0x267c; pub(crate) const M5_HW_DATA_INSERT_LEN: usize = 8; +/// The common 26A428 private cluster (J873, J813): status config 0x6d0 bytes, the secondary +/// root's second extra 0x60. +pub(crate) const COMMON26_PRIVATE_LAYOUT: PrivateClusterLayout = + private_cluster_layout(&ABI_ALLOCATION_SIZES_26A428); + +pub(crate) const COMMON26_POLICY_SIZE: usize = ABI_ALLOCATION_SIZES_26A428[ALLOC_POLICY]; + +const COMMON26_STATUS_CONFIG_SIZE: usize = ABI_ALLOCATION_SIZES_26A428[ALLOC_STATUS_CONFIG]; + +const COMMON26_STATUS_CONFIG_IN_STATUS_B: usize = + COMMON26_PRIVATE_LAYOUT.status_config - COMMON26_PRIVATE_LAYOUT.status_b; + +const COMMON26_STATUS_B_SPAN: usize = + COMMON26_PRIVATE_LAYOUT.roles[0].status_a - COMMON26_PRIVATE_LAYOUT.status_b; + +/// Status config +0x6c4 = 1 and +0x6c8 = the FW VA of status B +0xe71c. +const COMMON26_STATUS_CONFIG_LINK_WORD: usize = 0x6c4; +const COMMON26_STATUS_CONFIG_LINK_PTR: usize = 0x6c8; + +/// The policy block's FW VA of the private-cluster policy state (25G76 +0x3c98). +const COMMON26_POLICY_STATE_PTR: usize = 0x3d30; +/// The policy block's FW VA of the AON PTD (25G76 +0x6460). +const COMMON26_POLICY_AON_PTD_PTR: usize = 0x64d8; +/// The policy block's boot mach_absolute_time (25G76 +0x651c). +const COMMON26_POLICY_BOOT_TIMESTAMP: usize = 0x6c14; + +/// The 25G76 policy block (relative to the block) in the common 26A428 one, by the kext's +/// initPowerAndPerformanceData stores (the G18 backend's G18G_26A428_POLICY_BANDS). 26A428 +/// inserts 0x680 bytes between the AON PTD pointer and the boot timestamp; the 25G76 words +/// there have no place. +const COMMON26_POLICY_BANDS: &[LayoutBand] = &[ + band(0x0000, 0x009c, 0, Evidence::ProducerChecked), + band(0x00a0, 0x0200, 0x10, Evidence::ProducerChecked), + band(0x09b8, 0x295c, 0x10, Evidence::ProducerChecked), + band(0x2960, 0x29c1, 0x29, Evidence::ProducerChecked), + band(0x2a01, 0x2a05, 0x8d, Evidence::ProducerChecked), + band(0x2a08, 0x2e5c, 0x90, Evidence::ProducerChecked), + band(0x2e6c, 0x2e70, 0x9c, Evidence::ProducerChecked), + band(0x2e78, 0x2e7c, 0x74, Evidence::Inferred), + band(0x2f37, 0x2f43, -0x2b, Evidence::ProducerChecked), + band(0x2f43, 0x2f53, 0x71, Evidence::ProducerChecked), + band(0x2f58, 0x3260, 0x70, Evidence::ProducerChecked), + band(0x3c98, 0x3cac, 0x98, Evidence::ProducerChecked), + band(0x5cac, 0x5cb4, 0x98, Evidence::ProducerChecked), + band(0x63a4, 0x63f4, 0xa0, Evidence::ProducerChecked), + band(0x6458, 0x645c, 0x78, Evidence::ProducerChecked), + band(0x6460, 0x6468, 0x78, Evidence::ProducerChecked), + band(0x651c, 0x66a8, 0x6f8, Evidence::ProducerChecked), + band(0x6a18, 0x6a1c, 0x710, Evidence::ProducerChecked), +]; + +/// Policy fields the common 26A428 interface added: macOS sets them on J873 where the +/// relocated 25G76 words leave zeros. +0x64e0 is the AON row tag (RT0g, set from the board). +const COMMON26_POLICY_NEW_WORDS: [(usize, u32); 10] = [ + (0x009c, 1), (0x00a0, 1), (0x00a4, 1), (0x00a8, 1), + (0x0104, COMMON26_PERF_SCALE_VALUE), (0x0b4c, 1), + (0x2a88, 0x7148_0000), (0x2a90, 0x0064_0000), (0x30b0, 0x42c8_0000), + (0x6520, 1), +]; + +/// Status-config fields the common 26A428 interface added, as macOS sets them on J873: +/// +0x1dc/+0x1e0 after the secondary power-control inputs, +0x4e8 the two table enables. +const COMMON26_STATUS_CONFIG_NEW_WORDS: [(usize, u32); 3] = + [(0x1dc, 1), (0x1e0, 29000), (0x4e8, 0x0001_0001)]; + +/// Region-C fields the common 26A428 interface added, as macOS sets them on J873 (+0x38 is +/// the new feature bit; the M5 Pro's region C carries +0x9dc and +0xe0c at +0x9e0 and +/// +0x1c30). +const COMMON26_REGION_C_NEW_WORDS: [(usize, u32); 4] = + [(0x038, 1), (0x9b0, 0x200), (0x9dc, 50), (0xe0c, 250)]; + +/// The InitData layout a board's firmware build reads. +pub(crate) struct Graph { + /// The root, main-config and channel-table encodings. + pub(crate) profile: RootProfile, + pub(crate) private: PrivateClusterLayout, + pub(crate) shared: SharedClusterLayout, + pub(crate) policy_size: usize, + /// The five shared blocks main +0x254 points at. + pub(crate) bundle_view_offsets: [usize; 5], + pub(crate) bundle_view_extents: [usize; 5], + pub(crate) work_ring_offsets: [usize; WORK_CHANNEL_COUNT], + /// Each role's channel-state grid in the private cluster. + pub(crate) state_grids: [usize; 2], + /// Region C's power-assert tally. + pub(crate) power_assert_tally: usize, +} + +const fn state_grids(layout: &PrivateClusterLayout) -> [usize; 2] { + [ + layout.dm_ring_states[0], + layout.secondary_control_state - WORK_STATE_GRID_OFFSETS[CONTROL_CHANNEL_INDEX], + ] +} + +/// The M5 Pro's 26A428 build (J714, J716c, J775D). +pub(crate) const M5_GRAPH: Graph = Graph { + profile: RootProfile::J714G17S26A428, + private: M5_PRIVATE_LAYOUT, + shared: M5_SHARED_LAYOUT, + policy_size: M5_POLICY_SIZE, + bundle_view_offsets: M5_BUNDLE_VIEW_OFFSETS, + bundle_view_extents: M5_BUNDLE_VIEW_EXTENTS, + work_ring_offsets: M5_WORK_RING_BUNDLE_OFFSETS, + state_grids: M5_PRIVATE_STATE_GRIDS, + power_assert_tally: super::power::POWER_ASSERT_TALLY, +}; + +/// The common 26A428 build (J873's interface; the J813). +pub(crate) const COMMON26_GRAPH: Graph = Graph { + profile: RootProfile::J873G18G26A428, + private: COMMON26_PRIVATE_LAYOUT, + shared: COMMON26_SHARED_LAYOUT, + policy_size: COMMON26_POLICY_SIZE, + bundle_view_offsets: COMMON26_BUNDLE_VIEW_OFFSETS, + bundle_view_extents: COMMON26_BUNDLE_VIEW_EXTENTS, + work_ring_offsets: COMMON26_WORK_RING_BUNDLE_OFFSETS, + state_grids: state_grids(&COMMON26_PRIVATE_LAYOUT), + // The M5 Pro's tally sits in the block its region C moves from common +0xe10 to +0x1c34. + power_assert_tally: super::power::POWER_ASSERT_TALLY - 0x1c34 + 0xe10, +}; + +const _: () = { + assert!(M5_GRAPH.state_grids[0] == M5_PRIVATE_STATE_GRIDS[0]); + assert!(M5_GRAPH.state_grids[1] == state_grids(&M5_PRIVATE_LAYOUT)[1]); + assert!(COMMON26_PRIVATE_LAYOUT.total == 0x178000); + assert!(COMMON26_STATUS_CONFIG_IN_STATUS_B + COMMON26_STATUS_CONFIG_SIZE + <= COMMON26_STATUS_B_SPAN); +}; + +impl Graph { + pub(crate) fn is_common26(&self) -> bool { + self.profile == RootProfile::J873G18G26A428 + } +} + +/// 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. +fn encode_common26_primary_status_b( + fwctl_state: u64, + fwctl_ring: u64, + status_b: u64, + out: &mut [u8], +) -> Result<(), InitdataError> { + if out.len() != COMMON26_STATUS_B_SPAN { + return Err(InitdataError::BufferSize); + } + required_address(fwctl_state)?; + required_address(fwctl_ring)?; + required_address(status_b)?; + let config = COMMON26_STATUS_CONFIG_IN_STATUS_B; + 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]; + write_relocated_words(M5_25G76_STATUS_CONFIG_WORDS, 0, COMMON26_STATUS_CONFIG_BANDS, + status_config)?; + for (offset, value) in COMMON26_STATUS_CONFIG_NEW_WORDS { + put_u32(status_config, offset, value); + } + // 26A428 moved the last of the six 25G76 words at +0x588..+0x59c to the second place: + // on J873 +0x59c..+0x5b8 read 1, 700, 12, 5000, 2000, 5000, 0, 0, where the bands carry + // the board's six words to +0x5a4. + let words: [u32; 6] = core::array::from_fn(|index| { + let at = 0x5a4 + index * 4; + u32::from_le_bytes(status_config[at..at + 4].try_into().unwrap_or([0; 4])) + }); + for (index, value) in [words[0], words[5], words[1], words[2], words[3], words[4], 0, 0] + .into_iter().enumerate() { + put_u32(status_config, 0x59c + index * 4, value); + } + // The two 16x16 tables 26A428 copies in at +0x1e8 and +0x3e8 (the 25G76 words have none): + // zero first row and column, linear elsewhere, as on J873. + for row in 0..16 { + for col in 0..16 { + let active = row != 0 && col != 0; + put_u16(status_config, 0x1e8 + 2 * (row * 16 + col), + if active { (col * 0x1000 - 1) as u16 } else { 0 }); + status_config[0x3e8 + row * 16 + col] = if active { (col * 0x10 - 1) as u8 } else { 0 }; + } + } + 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); + 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. +pub(crate) fn encode_common26_private_cluster( + private_base: u64, + fwctl_state: u64, + out: &mut [u8], +) -> Result<(), InitdataError> { + let layout = COMMON26_PRIVATE_LAYOUT; + if out.len() != layout.total { + return Err(InitdataError::BufferSize); + } + required_address(private_base)?; + let private_end = private_base + .checked_add(layout.total as u64) + .ok_or(InitdataError::AddressOutOfRange)?; + required_address(private_end - 1)?; + required_address(fwctl_state)?; + let fwctl_ring = fwctl_state + .checked_add(COMMON26_FWCTL_RING_OFFSET as u64) + .ok_or(InitdataError::AddressOutOfRange)?; + required_address(fwctl_ring)?; + if fwctl_state < private_end && private_base < fwctl_ring + CONTROL_RECORD_SIZE as u64 { + return Err(InitdataError::PairAliasedObject); + } + 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, + &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, + &mut out[extra..extra + STATUS_BLOCK_SIZE])?; + put_u32(out, layout.control_state + CHANNEL_STATE_PRODUCER * CHANNEL_STATE_SPACING, + CONTROL_OPENING_PRIMARY_PRODUCER); + put_u32(out, layout.secondary_control_state + CHANNEL_STATE_PRODUCER * CHANNEL_STATE_SPACING, + CONTROL_OPENING_SECONDARY_PRODUCER); + 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. +pub(crate) fn encode_common26_bundle_policy( + board: &Board, + bindings: M5PolicyBindings, + out: &mut [u8], +) -> Result<(), InitdataError> { + let layout = COMMON26_SHARED_LAYOUT; + if out.len() < layout.brn_table { + return Err(InitdataError::BufferSize); + } + required_address(bindings.state)?; + required_address(bindings.aon_ptd)?; + let state_end = bindings.state.checked_add(STATUS_BLOCK_SIZE as u64) + .ok_or(InitdataError::AddressOutOfRange)?; + let aon_end = bindings.aon_ptd.checked_add(M5_AON_PTD_SIZE as u64) + .ok_or(InitdataError::AddressOutOfRange)?; + required_address(state_end - 1)?; + required_address(aon_end - 1)?; + if bindings.state < aon_end && bindings.aon_ptd < state_end { + return Err(InitdataError::PairAliasedObject); + } + out[layout.block_c18..layout.brn_table].fill(0); + let m5 = M5_25G76_SHARED_LAYOUT; + let m5_policy_end = m5.policy + ABI_ALLOCATION_SIZES_25G76[ALLOC_POLICY]; + for &(offset, value) in M5_25G76_POLICY_WORDS { + if (m5.block_1048..m5.block_e10).contains(&offset) { + put_u32(out, layout.block_1048 + (offset - m5.block_1048), value); + } else if !(m5.policy..m5_policy_end).contains(&offset) { + return Err(InitdataError::BufferSize); + } + } + let policy = &mut out[layout.policy..layout.policy + COMMON26_POLICY_SIZE]; + 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)?; + } + } + // The 25G76 block's 512 x 0x7fff u16 table at +0x1c97 moves with its band. + for index in 0..512 { + for (byte_index, byte) in 0x7fffu16.to_le_bytes().into_iter().enumerate() { + let source = 0x1c97 + index * 2 + byte_index; + let target = relocate_by_bands(COMMON26_POLICY_BANDS, source) + .ok_or(InitdataError::BufferSize)?; + *policy.get_mut(target).ok_or(InitdataError::BufferSize)? = byte; + } + } + for (offset, value) in COMMON26_POLICY_NEW_WORDS { + put_u32(policy, offset, value); + } + // The AON row the policy names: the board's first record (RT0g on J813 and J873). + let record = board.aon_records.first().ok_or(InitdataError::BufferSize)?; + put_u32(policy, 0x64e0, record[0]); + 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); + Ok(()) +} + +/// Encode a common 26A428 region C from the board's 25G76 one: the fields from +0x38 on move up +/// by four, plus the fields 26A428 added. Unlike [`encode_common26_region_c`] it keeps the +/// board's own tunables (standby timer, minimum confidence, state counts). +pub(crate) fn encode_common26_board_region_c(out: &mut [u8]) -> Result<(), InitdataError> { + if out.len() != REGION_C_SIZE { + return Err(InitdataError::BufferSize); + } + encode_region_c_for_profile(RootProfile::J813G17G25G76, out)?; + let shifted = ABI_ALLOCATION_SIZES_25G76[ALLOC_REGION_C]; + if out[shifted..].iter().any(|byte| *byte != 0) { + return Err(InitdataError::BufferSize); + } + out.copy_within(0x38..shifted, 0x3c); + out[0x38..0x3c].fill(0); + for (offset, value) in COMMON26_REGION_C_NEW_WORDS { + put_u32(out, offset, value); + } + Ok(()) +} + /// The board's offset of a field at `offset` of the M5 Pro's HW data: below the insert they /// agree, past it a board without the insert has the field eight bytes lower. const fn hw_offset(board: &Board, offset: usize) -> usize { @@ -1776,9 +2092,8 @@ const fn hw_offset(board: &Board, offset: usize) -> usize { pub(crate) struct Board { /// The chip ID the HW data names. chip_id: u32, - /// The InitData interface version the board's firmware checks at root +0x00, where it - /// differs from the M5 Pro's ([`M5_INITDATA_VERSION_WORD`]). - pub(crate) interface_version: Option, + /// The InitData layout the board's firmware build reads. + pub(crate) graph: &'static Graph, /// Whether the board's HW data has the M5 Pro firmware's eight bytes at /// [`M5_HW_DATA_INSERT`]; the common 26A428 interface (J813) has not. hw_data_insert: bool, @@ -1819,6 +2134,9 @@ pub(crate) struct Board { mcc_mask: Option, /// The two topology words (16, and the core count), where the board sets them. topology_words: Option<(u32, u32)>, + /// The board's own 25G76 HW data words where the inherited ones are the M5 Pro's, at their + /// offsets in the board's HW data. + hw_words: &'static [(usize, u32)], /// The hotspot at which the thermal governor's fan curve starts, millidegrees C. pub(crate) fan_curve_start_mc: i32, /// Whether the host encodings are the G17D ones measured on the M5 Ultra @@ -1832,7 +2150,7 @@ pub(crate) struct Board { /// M5 Pro: two clusters of ten cores. pub(crate) const J714: Board = Board { chip_id: 0x6050, - interface_version: None, + graph: &M5_GRAPH, hw_data_insert: true, clusters: 2, gpu_cores: 20, @@ -1851,6 +2169,7 @@ pub(crate) const J714: Board = Board { idle_standby_us: None, mcc_mask: None, topology_words: None, + hw_words: &[], fan_curve_start_mc: 84_000, g17d_forms: false, completion_inputs: true, @@ -1859,7 +2178,7 @@ pub(crate) const J714: Board = Board { /// M5 Max: four clusters of ten cores. pub(crate) const J716C: Board = Board { chip_id: 0x6050, - interface_version: None, + graph: &M5_GRAPH, hw_data_insert: true, clusters: 4, gpu_cores: 40, @@ -1880,6 +2199,7 @@ pub(crate) const J716C: Board = Board { // The ADT GPU node's active-mcc-bit-vector: eight memory cache controllers. mcc_mask: Some(0xff), topology_words: Some((16, 40)), + hw_words: &[], fan_curve_start_mc: 84_000, g17d_forms: false, completion_inputs: false, @@ -1891,7 +2211,7 @@ const J775D_PERF_FLOOR: usize = 3; /// M5 Ultra: two dies of four clusters of ten cores (G17D). Values from the J775D ADT. pub(crate) const J775D: Board = Board { chip_id: 0x6050, - interface_version: None, + graph: &M5_GRAPH, hw_data_insert: true, clusters: 8, gpu_cores: 80, @@ -1916,6 +2236,7 @@ pub(crate) const J775D: Board = Board { // The ADT GPU node's active-mcc-bit-vector: eight memory cache controllers. mcc_mask: Some(0xff), topology_words: None, + hw_words: &[], // The SMC raises the fans in its automatic mode from a 97 C hotspot under a sustained // full-chip load, with the CPU and GPU still at their top states: the curve starts there, // above the SMC's own start rather than below it. @@ -1930,8 +2251,8 @@ pub(crate) const J775D: Board = Board { pub(crate) const J813: Board = Board { chip_id: 0x8142, // The J813's 26A428 firmware compares root +0x00 with 0x0c8b_4339_83a0_04c0 (movz/movk at - // TEXT 0x21ed8 in both roles), the common 26A428 interface J873 also runs, not the M5 Pro's. - interface_version: Some(COMMON26_INITDATA_VERSION_WORD), + // TEXT 0x21ed8 in both roles): the common 26A428 interface J873 also runs, not the M5 Pro's. + graph: &COMMON26_GRAPH, // The common 26A428 HW data, without the M5 Pro's insert. hw_data_insert: false, clusters: 1, @@ -1950,8 +2271,11 @@ pub(crate) const J813: Board = Board { calibration: None, controller_defaults: None, idle_standby_us: None, - mcc_mask: None, + // One memory cache controller (/arm-io/sgx active-mcc-bit-vector; 25G76 +0xf8c = 1). + mcc_mask: Some(0x1), + // The J813's 25G76 HW data has 6 at +0xf28 (J714's 12), common 26A428 +0x1044. topology_words: None, + hw_words: &[(0x1044, 6)], fan_curve_start_mc: 84_000, g17d_forms: false, completion_inputs: false, @@ -2176,9 +2500,11 @@ const J714_REGISTER_WINDOWS: [M5RegisterWindowSpec; 30] = { /// J813: J714's 26A428 slots at the J813's addresses, matched through its ADT and its 25G76 /// windows (M5_25G76_REGISTER_WINDOWS: from the ID registers up, 26A428 numbers each GPU block /// five slots higher). Slots 4 and 11 sit where J714's do, AIC + 0x68000 and MCC reg 9 + -/// 0x8000. Bring-up: slot 20, per cluster on T6050, is left out. +/// 0x8000. Bring-up: slot 20, per cluster on T6050, is left out. From the common 26A428 map +/// (J873, same GPU base): slot 17 0x36000, DPEP (29) and ADCLK (30), and slot 32, the J813's +/// 25G76 slot 27, which J714's list lacks. const J813_GPU_BASE: u64 = 0x5_8000_0000; -const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 29] = { +const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 32] = { const G: u64 = J813_GPU_BASE; [ jw(0, &[0x3_8101_4000], 0x4000, 0, 2), @@ -2190,13 +2516,16 @@ const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 29] = { jw(12, &[0x4_2165_c000], 0x4000, 0, 2), jw(14, &[0x3_8028_0000], 0x8000, 0, 0), jw(15, &[0x3_8c84_0000], 0x24000, 0, 2), - // 25G76 maps 0x21400; the 26A428 firmware reads G + 0x25148 at bootstrap. - jw(17, &[G], 0x28000, 0, 2), + // 25G76 maps 0x21400, the common 26A428 map 0x36000. + jw(17, &[G], 0x36000, 0, 2), jw(18, &[G + 0xeb_4000], 0x4000, 0, 2), jw(22, &[G + 0x100_0000], 0x8000, 0, 2), jw(23, &[G + 0xf0_0000], 0x10000, 0, 2), jw(24, &[G + 0xec_0000], 0x10000, 0, 2), + jw(29, &[G + 0x1800], 0x27f8, 0, 0), + jw(30, &[G + 0xec_2200], 0xfc, 0, 0), jw(31, &[G + 0xd0_4000], 0x8000, 0xd0_4000, 2), + jw(32, &[G + 0xd0_d000], 0x1000, 0xd0_d000, 2), jw(33, &[G + 0xd5_0000], 0x10000, 0xd5_0000, 2), jw(34, &[G + 0xd1_0000], 0x4000, 0xd1_0000, 2), jw(36, &[G + 0xd4_0000], 0x4000, 0xd4_0000, 2), @@ -2466,6 +2795,9 @@ pub(crate) fn encode_m5_hw_data( put_u32(out, 0x100c, word); put_u32(out, 0x1048, cores); } + for &(offset, value) in board.hw_words { + put_u32(out, offset, value); + } // The inherited J813 minimum parameter-buffer size is replaced per board. put_u32(out, hw_offset(board, 0x26b0), board.min_parameter_buffer); Ok(()) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 5c333e05f8788..365ad7ada4b83 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -172,6 +172,8 @@ impl WorkPublication<'_> { /// Session-long storage and mappings. Aliases are released before their backing objects. pub(super) struct InitData { m5: bool, + /// The M5 firmware build's layout, where [`Self::m5`]. + graph: &'static super::fw::m5::initdata::Graph, outer_credits: Option>, // Live publication uses interior writes under the device mutex. This owner is Send but not // Sync, so safe host code cannot issue those writes concurrently through shared references. @@ -284,6 +286,7 @@ impl InitData { pub(super) fn new() -> Self { Self { m5: false, + graph: &super::fw::m5::initdata::M5_GRAPH, outer_credits: None, _single_publisher: core::marker::PhantomData, aliases: KVec::new(), @@ -1655,17 +1658,17 @@ impl InitData { } fn main_config_offset(&self, role: Role) -> usize { - if self.m5 { super::fw::m5::initdata::M5_MAIN_BUNDLE_OFFSETS[role as usize] } + if self.m5 { self.graph.shared.main_config[role as usize] } else { abi::bundle::MAIN_CONFIG[role as usize] } } fn work_ring_offset(&self, channel: usize) -> usize { - if self.m5 { super::fw::m5::initdata::M5_WORK_RING_BUNDLE_OFFSETS[channel] } + if self.m5 { self.graph.work_ring_offsets[channel] } else { abi::work_ring_offset(channel) } } fn state_offset(&self, role: Role) -> usize { - if self.m5 { return super::fw::m5::initdata::M5_PRIVATE_STATE_GRIDS[role as usize]; } + if self.m5 { return self.graph.state_grids[role as usize]; } match role { Role::Primary => abi::private::PRIMARY_STATE, Role::Secondary => abi::private::SECONDARY_STATE, @@ -1673,7 +1676,7 @@ impl InitData { } fn status_offset(&self, role: Role) -> usize { - if self.m5 { return super::fw::m5::initdata::M5_PRIVATE_STATUS_A[role as usize]; } + if self.m5 { return self.graph.private.roles[role as usize].status_a; } match role { Role::Primary => self.status_a, Role::Secondary => abi::private::SECONDARY_STATUS_A, @@ -1770,13 +1773,13 @@ impl InitData { next: u32, ) -> Result { let ring_base = if self.m5 { - super::fw::m5::initdata::M5_CONTROL_RING_BUNDLE_OFFSETS[0] + self.graph.shared.control_ring[0] } else { self.main_config_offset(Role::Primary) + abi::MainConfig::CONTROL_RING }; let offset = ring_base + (slot as usize).checked_mul(record.len()).ok_or(EINVAL)?; let ring = self.object(self.bundle)?.pointer(offset, record.len())?; let power = self .object(self.region_c)? - .pointer(if self.m5 { super::fw::m5::power::POWER_ASSERT_TALLY } + .pointer(if self.m5 { self.graph.power_assert_tally } else { abi::RegionC::POWER_ASSERT_TALLY }, 4)?; let producer = self.control_producer()?; // SAFETY: The checked pointers remain valid for the lifetime of this non-Sync owner. @@ -1827,13 +1830,13 @@ impl InitData { fence(Ordering::Acquire); Ok(self .object(self.cluster)? - .word(m::M5_PRIVATE_LAYOUT.status_b + m::M5_PROGRESS)? + .word(self.graph.private.status_b + m::M5_PROGRESS)? .load(Ordering::Relaxed)) } fn recovery_state_offset(&self) -> usize { if self.m5 { - super::fw::m5::initdata::M5_PRIVATE_LAYOUT.status_b + self.graph.private.status_b + super::fw::m5::initdata::M5_RECOVERY_STATE } else { self.status_b + abi::PrimaryStatusB::RECOVERY_STATE } } @@ -1842,7 +1845,7 @@ impl InitData { fn recovery_view_words(&self, view: usize, offset: usize) -> Result<[u32; N]> { let bundle = self.object(self.bundle)?; let extent = if self.m5 { - *super::fw::m5::initdata::M5_BUNDLE_VIEW_EXTENTS.get(view).ok_or(EINVAL)? + *self.graph.bundle_view_extents.get(view).ok_or(EINVAL)? } else { *abi::recovery::VIEW_EXTENTS.get(view).ok_or(EINVAL)? }; @@ -1851,7 +1854,7 @@ impl InitData { Some(views) => (self.object(*views.get(view).ok_or(EINVAL)?)?, 0), None if self.m5 => ( bundle, - *super::fw::m5::initdata::M5_BUNDLE_VIEW_OFFSETS.get(view).ok_or(EINVAL)?, + *self.graph.bundle_view_offsets.get(view).ok_or(EINVAL)?, ), None => (bundle, *abi::bundle::VIEWS.get(view).ok_or(EINVAL)?), }; @@ -1879,7 +1882,7 @@ impl super::recovery::Memory for InitData { } fn fault_sources(&self) -> Result { let mask_offset = if self.m5 { - super::fw::m5::initdata::M5_PRIVATE_LAYOUT.status_b + self.graph.private.status_b + super::fw::m5::initdata::M5_RECOVERY_SLOT_MASK } else { self.status_b + abi::PrimaryStatusB::RECOVERY_SLOT_MASK @@ -1927,7 +1930,7 @@ impl super::recovery::Memory for InitData { Ok(self .object(self.cluster)? .word(if self.m5 { - super::fw::m5::initdata::M5_PRIVATE_LAYOUT.status_b + self.graph.private.status_b + super::fw::m5::initdata::M5_HOST_RECOVERY } else { self.status_b + abi::PrimaryStatusB::HOST_RECOVERY @@ -1956,7 +1959,7 @@ impl super::recovery::Memory for InitData { Some(views) => (self.object(*views.get(4).ok_or(EINVAL)?)?, 0), None if self.m5 => ( self.object(self.bundle)?, - super::fw::m5::initdata::M5_BUNDLE_VIEW_OFFSETS[4], + self.graph.bundle_view_offsets[4], ), None => (self.object(self.bundle)?, abi::bundle::VIEWS[4]), }; @@ -1982,7 +1985,7 @@ impl super::recovery::Memory for InitData { } let cluster = self.object(self.cluster)?; let base = if self.m5 { - super::fw::m5::initdata::M5_PRIVATE_LAYOUT.status_b + self.graph.private.status_b + super::fw::m5::initdata::M5_RECOVERY_INFO } else { self.status_b + abi::PrimaryStatusB::RECOVERY_INFO @@ -2014,9 +2017,10 @@ impl InitData { /// M5 only: the GPU's current power draw in mW, as the firmware's power controller reports it /// (integer word of the shared power-policy block). pub(super) fn m5_gpu_power_mw(&self) -> Result { - if !self.m5 { return Err(EINVAL); } + // The power word's place is the M5 Pro build's. + if !self.m5 || self.graph.is_common26() { return Err(EINVAL); } Ok(self.object(self.bundle)? - .word(super::fw::m5::initdata::M5_SHARED_LAYOUT.policy + super::thermal::POLICY_GPU_POWER)? + .word(self.graph.shared.policy + super::thermal::POLICY_GPU_POWER)? .load(Ordering::Relaxed)) } @@ -2025,7 +2029,7 @@ impl InitData { pub(super) fn m5_firmware_clamped(&self) -> Result { if !self.m5 { return Err(EINVAL); } let block = self.object(self.bundle)?; - let base = super::fw::m5::initdata::M5_SHARED_LAYOUT.policy; + let base = self.graph.shared.policy; let mask = block.word(base + super::thermal::POLICY_LIMITER_MASK)?.load(Ordering::Relaxed); let limit = block.word(base + super::thermal::POLICY_LIMITER_LIMIT)?.load(Ordering::Relaxed); Ok(super::thermal::is_emergency_clamp(mask, limit)) diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index 2b5d26fe3c951..c5f0b7066e1a2 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -41,15 +41,19 @@ impl InitData { /// Diagnostic-only read of an aligned word within the existing policy allocation. /// Callers hold the device state mutex, retaining this mapping for the entire sample. pub(crate) fn m5_policy_telemetry_word(&self, offset: usize) -> Result { - if !self.m5 || offset & 3 != 0 || offset.checked_add(4).is_none_or(|end| end > m::M5_POLICY_SIZE) { + if !self.m5 || offset & 3 != 0 + || offset.checked_add(4).is_none_or(|end| end > self.graph.policy_size) { return Err(EINVAL); } - let at = m::M5_SHARED_LAYOUT.policy.checked_add(offset).ok_or(EINVAL)?; + let at = self.graph.shared.policy.checked_add(offset).ok_or(EINVAL)?; Ok(self.object(self.bundle)?.word(at)?.load(Ordering::Relaxed)) } pub(crate) fn note_m5_submission_history(&self) -> Result { if !self.m5 { return Err(EINVAL); } + // The submission history is the M5 Pro build's status config +0x73c..; the common + // 26A428 one (0x6d0 bytes) has no such table. + if self.graph.is_common26() { return Ok(()); } let private = self.object(self.cluster)?; let base = m::M5_PRIVATE_LAYOUT.status_config; for _ in 0..64 { @@ -86,6 +90,8 @@ impl InitData { use mmu::{PROT_FW_SHARED_RW as RW, PROT_FW_SHARED_RO as RO, PROT_GPU_SHARED_RW as GPU}; self.m5 = true; + self.graph = board.graph; + let common26 = board.graph.is_common26(); // TA, fragment and compute share the compute work ring per priority; every // preparation's claim on it is counted from the first publication on. self.outer_credits = Some(super::outer_credit::Credits::new()?); @@ -95,8 +101,8 @@ impl InitData { KernelObject::new(dev, uat.kernel_vm(), Placement::At(address), size, mmu::PROT_FW_PRIV_RW, CpuMap::WriteBack) }; - let private = m::M5_PRIVATE_LAYOUT; - let shared = m::M5_SHARED_LAYOUT; + let private = board.graph.private; + let shared = board.graph.shared; self.cluster = self.add(fixed(PRIVATE, private.total, RW)?)?; self.region_c = self.add(fixed(REGION_C, mmu::UAT_PGSZ, RO)?)?; self.roots = self.add(fixed(ROOTS, 0xc000, RO)?)?; @@ -135,9 +141,18 @@ impl InitData { } m::encode_common26_aon_ptd(board, &mut self.object_mut(aon)?.bytes_mut()[..m::M5_AON_PTD_SIZE]) .map_err(|_| EINVAL)?; - m::encode_m5_region_c(self.object_mut(self.region_c)?.bytes_mut()).map_err(|_| EINVAL)?; - m::encode_m5_private_cluster(PRIVATE, FWCTL, - &mut self.object_mut(self.cluster)?.bytes_mut()[..private.total]).map_err(|_| EINVAL)?; + if common26 { + 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, + &mut self.object_mut(self.cluster)?.bytes_mut()[..private.total]) + .map_err(|_| EINVAL)?; + } else { + m::encode_m5_region_c(self.object_mut(self.region_c)?.bytes_mut()).map_err(|_| EINVAL)?; + m::encode_m5_private_cluster(PRIVATE, FWCTL, + &mut self.object_mut(self.cluster)?.bytes_mut()[..private.total]).map_err(|_| EINVAL)?; + } let windows = board.register_windows(); let windows = &windows[..board.register_window_count()]; for (index, window) in windows.iter().enumerate() { @@ -172,9 +187,14 @@ impl InitData { m::set_m5_enabled_cores(board, &mut self.object_mut(self.bundle)?.bytes_mut()[..m::M5_HW_DATA_SIZE], enabled_cores) .map_err(|_| EINVAL)?; - m::encode_m5_bundle_policy(m::M5PolicyBindings { + let bindings = m::M5PolicyBindings { state: PRIVATE + private.policy_state as u64, aon_ptd: AON, boot_timestamp: 0, - }, self.object_mut(self.bundle)?.bytes_mut()).map_err(|_| EINVAL)?; + }; + if common26 { + m::encode_common26_bundle_policy(board, bindings, self.object_mut(self.bundle)?.bytes_mut()) + } else { + m::encode_m5_bundle_policy(bindings, self.object_mut(self.bundle)?.bytes_mut()) + }.map_err(|_| EINVAL)?; // The controller configuration applies where the part's leakage calibration matches the // one it was made for, or on a board whose configuration holds for every part. let calibrated = match board.calibration { @@ -207,9 +227,9 @@ impl InitData { [PRIVATE + private.status_config as u64, PRIVATE + private.secondary_extra_1 as u64] }, }; - let profile = m::RootProfile::J714G17S26A428; + let profile = board.graph.profile; let channels = m::derive_channel_table_for_profile(profile, role, - PRIVATE + m::M5_PRIVATE_STATE_GRIDS[index] as u64, + PRIVATE + board.graph.state_grids[index] as u64, instance.status_a, instance.main_config, BUNDLE); let offset = shared.main_config[index]; m::encode_main_config_for_profile(profile, role, &addresses, &instance, @@ -220,10 +240,6 @@ impl InitData { m::encode_root_for_profile(profile, role, &addresses, &instance, &mut self.object_mut(self.roots)?.bytes_mut()[offset..offset + m::M5_ROOT_SIZE]) .map_err(|_| EINVAL)?; - if let Some(version) = board.interface_version { - self.object_mut(self.roots)?.bytes_mut()[offset..offset + 8] - .copy_from_slice(&version.to_le_bytes()); - } } fence(Ordering::SeqCst); dev_info!(dev.as_ref(), "M5 firmware graph ready: private {:#x}+{:#x}, shared {:#x}+{:#x}\n", From 1b45e21536db0b0db962cbb3a3af6f46ce23dd10 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 7 Nov 2026 18:34:36 +0100 Subject: [PATCH 09/30] drm/asahi: g17: M5: fill the J813 InitData kick-queue seed and entry count (bring-up) macOS 27.0's initFirmwareData also writes the KSM kick-queue setup into InitData: a u64 timestamp seed at bundle +0x2728 and the ring's entry count at +0x2730, read from the accelerator's kick-queue config block (createKSMKickQueues loads them at +0x10bc0 and +0x10bb8). The firmware copies the seed into its context +0xb80 and seeds all 128 queue states from it; the Linux J813 encoder left both fields zero. Fill them on the common 26A428 graph (the J813) from this driver's own kick protocol: the first kick of every queue is KickTimestamp::FIRST and every ring has KICK_SLOTS entries, so the firmware's expected counts line up with what the host will send. These are the logical 0x2730/0x2738 offsets, which hw_offset maps to the physical 0x2728/0x2730 on a no-insert board like the J813. Tried on the J813 (trial module try22): both roles ACK InitData and GFX still takes the same external abort at 0x5_8002_1148, so these fields are not what keeps the KSM block down under 27.0; keep the encoding as the protocol-correct one. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index 7a62dfb14d487..a693bc79c3577 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -9,6 +9,8 @@ use core::ops::Range; use core::result::Result; +use crate::g17::fw::kick::{KickTimestamp, KICK_SLOTS}; + pub(crate) const ROOT_SIZE_PRIMARY: usize = 0x0b8; pub(crate) const ROOT_SIZE_SECONDARY: usize = 0x0c8; @@ -2788,6 +2790,14 @@ pub(crate) fn encode_m5_hw_data( put_u32(out, 0x2660, 0); put_u32(out, hw_offset(board, 0x2728), 0); put_u32(out, hw_offset(board, 0x272c), 1); + // On the J813 macOS 27.0's initFirmwareData also seeds the KSM kick queues + // here: the NextTimestamp of each queue's first kick and the ring's entry + // count, taken from the driver's kick-queue setup. The firmware copies the + // timestamp to context +0xb80, where it seeds all 128 queue states. + if board.graph.is_common26() { + put_u64(out, hw_offset(board, 0x2730), KickTimestamp::FIRST.get()); + put_u32(out, hw_offset(board, 0x2738), KICK_SLOTS as u32); + } if let Some(mask) = board.mcc_mask { put_u64(out, 0x10a8, mask); } From e5bdd2023f9e69cba3cebf4700b660b4dbf900a4 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 10:15:38 +0100 Subject: [PATCH 10/30] drm/asahi: g17: M5: follow macOS's order for the gating and the init ack aura's trace of macOS 27.0 on the J813 (FINDINGS.md) shows two things this driver did differently, and on the J813 both are now as macOS does them: - The Fender gating is cleared once both coprocessors run, not before them. macOS writes the SGX start control at platform init, powers ps_gfx, starts GFX1's then GFX's ASC, exchanges RTKit HELLO, and only then reads 0x6 from the gating control and writes 0. prepare_m5_firmware() is split so the start control can be set on its own. - After the firmware acknowledges its initialization root, macOS answers on the doorbell endpoint with the acknowledgement type and the host bit (0x89000000000000 on endpoint 0x21), to both roles. This driver never sent it. Neither changes the J813's crash: both roles still acknowledge and the primary still takes the same SError on the KSM block (FAR 0x580021148) immediately after its own acknowledgement, before it can process the doorbell answer. They are kept because they match the machine's own firmware. The other M5 boards keep the order they had: prepare_m5_firmware() after the coprocessor boot, and no answer. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/Makefile | 2 +- drivers/gpu/drm/asahi/g17/mod.rs | 55 ++++++++- drivers/gpu/drm/asahi/g17/pmpdash.c | 176 ++++++++++++++++++++++++++++ drivers/gpu/drm/asahi/regs.rs | 28 ++--- 4 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/pmpdash.c diff --git a/drivers/gpu/drm/asahi/Makefile b/drivers/gpu/drm/asahi/Makefile index e50a4d768369f..2de04f7798123 100644 --- a/drivers/gpu/drm/asahi/Makefile +++ b/drivers/gpu/drm/asahi/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_ASAHI) += aurora.o -aurora-y := asahi_core.o fwid.o sysfs.o g17/sync_input.o g17/sync_output.o g17/sync_scope.o g17/trace.o g17/clpc.o g17/fans.o +aurora-y := asahi_core.o fwid.o sysfs.o g17/sync_input.o g17/sync_output.o g17/sync_scope.o g17/trace.o g17/clpc.o g17/fans.o g17/pmpdash.o CFLAGS_g17/trace.o := -I$(src)/g17 aurora-$(CONFIG_DRM_ASAHI_G15) += g15/glue.o diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 4ee9dd8b3f8f9..b86a6965d1681 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -298,6 +298,10 @@ const MSG_INITDATA: u64 = 0x81 << MSG_TYPE_SHIFT; const MSG_INITDATA_ADDR_MASK: u64 = (1 << 44) - 1; /// Firmware message acknowledging [`MSG_INITDATA`]. const MSG_INITDATA_ACK: u64 = 0x09; +/// Host message on the doorbell endpoint that answers [`MSG_INITDATA_ACK`]: the acknowledgement +/// type with the host bit. macOS 27.0 on the J813 sends it to both roles a few milliseconds after +/// it hands them their initialization root (aura's trace: 0x89000000000000 on endpoint 0x21). +const MSG_INITDATA_DONE: u64 = 0x89 << MSG_TYPE_SHIFT; /// Firmware message announcing new records in the event and trace rings. const MSG_EVENTS: u64 = 0x42 << MSG_TYPE_SHIFT; /// Primary processor notification for newly published device-control records. @@ -1802,6 +1806,23 @@ impl Gpu { uat.kernel_lower_dynamic_range(), )) .inspect_err(at("lower links"))?; + // The J813 in native order, from aura's trace of macOS 27.0 on that machine: the start + // control is written first (macOS does it at platform init), the coprocessors are started + // next, and the Fender gating is cleared only once both of them run -- not before them. + // The other boards prepare both after the coprocessor boot (prepare_m5_firmware). + if cfg.chip_id == 0x8142 { + regs.set_m5_start_control().inspect_err(at("M5 start control"))?; + // Bring-up trial: force the ps_gfx transition macOS performs from off (the domain + // is inherited on here), then re-apply the SGX preparation the cycle resets. + extern "C" { + fn asahi_j813_pmp_agx_power_cycle() -> core::ffi::c_int; + } + // SAFETY: the helper only ioremaps and pokes the PMGR GPU power-state register. + if unsafe { asahi_j813_pmp_agx_power_cycle() } != 0 { + dev_warn!(dev.as_ref(), "M5: GFX power cycle incomplete\n"); + } + regs.set_m5_start_control().inspect_err(at("M5 start control after cycle"))?; + } let primary = Coprocessor::new(pdev, dev, cfg, &uat, shared, Role::Primary) .inspect_err(|e| dev_err!(dev.as_ref(), "Primary coprocessor setup failed: {:?}\n", e))?; let secondary = Coprocessor::new(pdev, dev, cfg, &uat, shared, Role::Secondary) @@ -1874,6 +1895,15 @@ impl Gpu { session.primary.boot().inspect_err(at("primary boot"))?; let secondary = session.secondary.as_mut().ok_or(EINVAL)?; secondary.boot().inspect_err(at("secondary boot"))?; + // Both coprocessors run: clear the J813's Fender gating here, where macOS 27.0 clears it + // (it reads 0x6 and writes 0 after both ASCs are running and the first mailbox exchange). + if cfg.chip_id == 0x8142 { + session + .bridge + .regs + .enable_g17_gating() + .inspect_err(at("G17 gating after the coprocessor boot"))?; + } // The firmware built its root and kernel-window tables while booting. session.uat.invalidate_reserved_tables(); let root_entries = session @@ -1905,11 +1935,24 @@ impl Gpu { .confirm_firmware_root_entries(root_entries) .inspect_err(at("root confirmation"))?; session.uat.mirror_secondary_root().inspect_err(at("secondary root"))?; - session - .bridge - .regs - .prepare_m5_firmware() - .inspect_err(at("M5 preparation"))?; + if cfg.chip_id == 0x8142 { + // Bring-up trial: report AGX on through the PMP dashboard in native order (SGX + // start control and gating set, before InitData), as macOS does right after the + // platform power-on write. + extern "C" { + fn asahi_j813_pmp_report_agx(on: bool) -> core::ffi::c_int; + } + // SAFETY: the helper only ioremaps and pokes the mcc dashboard windows. + if unsafe { asahi_j813_pmp_report_agx(true) } != 0 { + dev_warn!(dev.as_ref(), "M5: PMP AGX report failed\n"); + } + } else { + session + .bridge + .regs + .prepare_m5_firmware() + .inspect_err(at("M5 preparation"))?; + } let ack = shared.variant.init_ack; let primary_root = session.init.root_va(Role::Primary)?; let secondary_root = session.init.root_va(Role::Secondary)?; @@ -1918,8 +1961,10 @@ impl Gpu { if cfg.chip_id == 0x8142 { let secondary = session.secondary.as_mut().ok_or(EINVAL)?; secondary.init(secondary_root, ack).inspect_err(at("secondary InitData"))?; + secondary.notify(MSG_INITDATA_DONE)?; dev_info!(dev.as_ref(), "M5 secondary initdata acknowledged\n"); session.primary.init(primary_root, ack).inspect_err(at("primary InitData"))?; + session.primary.notify(MSG_INITDATA_DONE)?; dev_info!(dev.as_ref(), "M5 primary initdata acknowledged\n"); } else { session.primary.init(primary_root, ack).inspect_err(at("primary InitData"))?; diff --git a/drivers/gpu/drm/asahi/g17/pmpdash.c b/drivers/gpu/drm/asahi/g17/pmpdash.c new file mode 100644 index 0000000000000..acfe0fc916a0c --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/pmpdash.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Bring-up only: PMP helpers for the J813 GPU. + * + * report: report the AGX device state to the T8142 PMP over the mcc dashboard + * (SOC-DEV-PS-REQ idx 0x27, SOC-DEV-PS-ACK idx 0x2f), the way macOS ApplePMGR + * does in native order right after the platform power-on write. + * + * power_cycle: force the ps_gfx power transition macOS performs from off. The + * M5's domain is inherited on (0x1f0023ff), so Linux's genpd never transitions + * it; macOS writes target 0xf with auto-PM off, then auto-PM on, around the PMP + * report. A transition edge may be what makes the PMP enable the sgx node's + * power/clock gates 515 (GFX-SGX) and 516 (GFX-BUSY). + */ + +#include +#include +#include +#include +#include + +int asahi_j813_pmp_report_agx(bool on); +int asahi_j813_pmp_agx_power_cycle(void); + +#define PTD_DB_BASE 0x3803c0000ULL +#define PTD_UPD_BASE 0x3803d0000ULL +#define PTD_WIN_SIZE 0x4000 + +#define PTD_PS_REQ 0x27 +#define PTD_PS_ACK 0x2f +#define PTD_META_FRESH BIT_ULL(1) + +#define AGX_BIT 12 + +#define SGX_PS_BASE 0x580e80000ULL +#define SGX_PS_REG 0x110 + +#define PS_TARGET GENMASK(3, 0) +#define PS_ACTUAL GENMASK(7, 4) +#define PS_WAS_PWRGATED BIT(8) +#define PS_WAS_CLKGATED BIT(9) +#define PS_DEV_DISABLE BIT(10) +#define PS_BUSY BIT(11) +#define PS_RESET BIT(12) +#define PS_AUTO_ENABLE BIT(28) +#define PS_FLAGS (PS_WAS_PWRGATED | PS_WAS_CLKGATED) + +static u64 ptd_read(void __iomem *db, u32 idx, u64 *meta) +{ + u64 v = readq(db + 16 * idx); + + *meta = readq(db + 16 * idx + 8); + return v; +} + +int asahi_j813_pmp_report_agx(bool on) +{ + u64 req, ack, meta, pre_ack, pre_meta, want; + ktime_t t0; + void __iomem *db, *upd; + bool fresh = false; + int ret = 0; + + db = ioremap(PTD_DB_BASE, PTD_WIN_SIZE); + upd = ioremap(PTD_UPD_BASE, PTD_WIN_SIZE); + if (!db || !upd) { + ret = -ENOMEM; + goto out; + } + + req = ptd_read(db, PTD_PS_REQ, &meta); + pre_ack = ptd_read(db, PTD_PS_ACK, &pre_meta); + want = on ? req | BIT_ULL(AGX_BIT) : req & ~BIT_ULL(AGX_BIT); + pr_info("asahi: PMP AGX %s: PS-REQ %#llx -> %#llx (pre-ACK %#llx meta %#llx)\n", + on ? "on" : "off", req, want, pre_ack, pre_meta); + writeq(want, upd + 8 * PTD_PS_REQ); + + t0 = ktime_get(); + for (;;) { + ack = ptd_read(db, PTD_PS_ACK, &meta); + if (meta & PTD_META_FRESH) + fresh = true; + if (!((ack ^ want) & BIT_ULL(AGX_BIT)) && + (fresh || ack != pre_ack)) { + pr_info("asahi: PMP AGX %s ACKed in %lld us (ACK %#llx meta %#llx fresh=%u)\n", + on ? "on" : "off", + ktime_us_delta(ktime_get(), t0), ack, meta, fresh); + break; + } + if (ktime_us_delta(ktime_get(), t0) > 15000000) { + pr_warn("asahi: PMP AGX %s NOT acked in 15 s (ACK %#llx meta %#llx fresh=%u)\n", + on ? "on" : "off", ack, meta, fresh); + ret = -ETIMEDOUT; + break; + } + usleep_range(100, 200); + } + +out: + if (upd) + iounmap(upd); + if (db) + iounmap(db); + return ret; +} + +static u32 ptd_ps_write(void __iomem *ps, u32 pstate, bool auto_enable) +{ + u32 cur = readl(ps); + u32 val = cur & ~(PS_TARGET | PS_FLAGS | PS_BUSY | PS_DEV_DISABLE | + PS_RESET | PS_AUTO_ENABLE); + + val |= FIELD_PREP(PS_TARGET, pstate); + if (auto_enable) + val |= PS_AUTO_ENABLE; + writel(val, ps); + return val; +} + +static int ptd_ps_wait(void __iomem *ps, u32 pstate, u32 *out) +{ + ktime_t t0 = ktime_get(); + u32 cur; + + for (;;) { + cur = readl(ps); + if (FIELD_GET(PS_ACTUAL, cur) == pstate) + break; + if (ktime_us_delta(ktime_get(), t0) > 20000) { + *out = cur; + return -ETIMEDOUT; + } + usleep_range(50, 100); + } + *out = cur; + return 0; +} + +int asahi_j813_pmp_agx_power_cycle(void) +{ + void __iomem *ps; + u32 val, cur; + int ret = 0; + + ps = ioremap(SGX_PS_BASE, 0x1000); + if (!ps) + return -ENOMEM; + + cur = readl(ps + SGX_PS_REG); + pr_info("asahi: GFX ps before cycle: %#x\n", cur); + + val = ptd_ps_write(ps + SGX_PS_REG, 0, false); + if (ptd_ps_wait(ps + SGX_PS_REG, 0, &cur)) { + pr_warn("asahi: GFX ps did not power-gate (wrote %#x, now %#x)\n", + val, cur); + ret = -EIO; + } else { + pr_info("asahi: GFX ps powered off (wrote %#x, now %#x)\n", val, cur); + } + + val = ptd_ps_write(ps + SGX_PS_REG, 0xf, false); + if (ptd_ps_wait(ps + SGX_PS_REG, 0xf, &cur)) { + pr_warn("asahi: GFX ps did not power on (wrote %#x, now %#x)\n", + val, cur); + ret = -EIO; + } else { + pr_info("asahi: GFX ps powered on, auto-PM off (wrote %#x, now %#x)\n", + val, cur); + } + + val = ptd_ps_write(ps + SGX_PS_REG, 0xf, true); + pr_info("asahi: GFX ps auto-PM on (wrote %#x)\n", val); + + iounmap(ps); + return ret; +} diff --git a/drivers/gpu/drm/asahi/regs.rs b/drivers/gpu/drm/asahi/regs.rs index 60e485f29f06b..c40c5d448f3f4 100644 --- a/drivers/gpu/drm/asahi/regs.rs +++ b/drivers/gpu/drm/asahi/regs.rs @@ -868,21 +868,21 @@ impl G17Resources { Ok(()) } - /// Enable dynamic power gating of idle G17 GPU cores. The firmware expects this before it is - /// initialized. - /// Prepares the GPU for the M5 firmware before its initialization: sets bit 0 of the - /// firmware start control, which reads 0x30000 or 0x30001 until then, and enables dynamic - /// core power gating. - pub(crate) fn prepare_m5_firmware(&self) -> Result { - { - let sgx = self.sgx.try_access().ok_or(ENODEV)?; - let sgx = (&*sgx).as_view(); - let value = sgx.try_read32(0xe90000)?; - if value != 0x30000 && value != 0x30001 { - return Err(EIO); - } - sgx.try_write32(value | 1, 0xe90000)?; + /// Sets bit 0 of the firmware start control, which reads 0x30000 or 0x30001 until then. + /// macOS writes it at platform init, long before the GPU's power-on and its coprocessors. + pub(crate) fn set_m5_start_control(&self) -> Result { + let sgx = self.sgx.try_access().ok_or(ENODEV)?; + let sgx = (&*sgx).as_view(); + let value = sgx.try_read32(0xe90000)?; + if value != 0x30000 && value != 0x30001 { + return Err(EIO); } + sgx.try_write32(value | 1, 0xe90000) + } + + /// Prepares the GPU for the M5 firmware: the start control, then dynamic core power gating. + pub(crate) fn prepare_m5_firmware(&self) -> Result { + self.set_m5_start_control()?; self.enable_g17_gating() } From 42499a13893d8b319a630ec81086bc46c882e717 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 12:50:03 +0100 Subject: [PATCH 11/30] drm/asahi: g17: M5: the J813's own register map, from macOS 27.0's bundle aura can now park the macOS 27.0 guest right after its GPU bring-up and read the memory its firmware runs on, so the J813's hardware-data bundle no longer has to be inferred from another board's trace. Its register map, which the firmware reads to reach every block it drives, differed in four ways: - Thirteen slots this encoder filled are empty in macOS's map (4, 11, 18, 23, 29, 30, 38, 39, 42, 48, 52, 53 and 58), three it left empty are filled (8 at 0x3_a135_0000, 47 at sgx+0xe3_5000 and 56 at sgx+0x1_4400, which nothing mapped before), and slot 17 spans 0x21400, not 0x36000: it ends just above the KSM block the firmware programs. - The device VAs are packed in slot order, and the guard page between windows follows a window's own bytes before either is rounded up to a page. Rounding first pushed every window after slot 32 -- whose 0x1000 bytes sit at a 0x1000 offset -- half a page too high. The other boards keep the spacing they had. - Every slot carries a flag whether or not a window uses it. The flag vector is the board's, so only a board whose own firmware's map has been read has one. - Ten words of the hardware data that the inherited 25G76 template and the generic encoding leave different, including the pair at 0x2720 (whose values were the other way round) and the chip-id words at 0xfa8 and 0xfb4. With these the map this driver encodes and the windows it maps are byte for byte macOS's own, at macOS's own device VAs. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 63 ++++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index a693bc79c3577..f987b2298382a 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -2130,6 +2130,10 @@ pub(crate) struct Board { pub(crate) calibration: Option<[u32; 3]>, /// The policy controllers' configuration, where the board has one. pub(crate) controller_defaults: Option Result<(), InitdataError>>, + + /// The flag of every register-map slot, whether or not a window uses it. Only boards whose + /// own firmware's map has been read carry one. + pub(crate) register_slot_flags: Option<&'static [u32; COMMON26_HW_REGISTER_MAP_SLOTS]>, /// Region C's idle standby timer where the board's differs from the image's 1500 us, us. pub(crate) idle_standby_us: Option, /// The active memory cache controller mask, where it differs from the inherited 0xf. @@ -2164,6 +2168,7 @@ pub(crate) const J714: Board = Board { relative_frequency: relative_frequency(&J714_GPU_PERF_STATES, 1), aon_records: &J714_AON_RECORDS, register_windows: &J714_REGISTER_WINDOWS, + register_slot_flags: None, min_parameter_buffer: 0x0020_0000, render_pool: (0x40_0000, 1 << 32), calibration: Some([0xd2035701, 0xac3a1280, 0x0a281d81]), @@ -2192,6 +2197,7 @@ pub(crate) const J716C: Board = Board { relative_frequency: relative_frequency(&J716C_GPU_PERF_STATES, 1), aon_records: &J716C_AON_RECORDS, register_windows: &J716C_REGISTER_WINDOWS, + register_slot_flags: None, min_parameter_buffer: 0x0030_0000, render_pool: (0x80_0000, 1 << 33), // Its own controller configuration, for a part with this calibration. @@ -2227,6 +2233,7 @@ pub(crate) const J775D: Board = Board { relative_frequency: relative_frequency(&J775D_GPU_PERF_STATES, J775D_PERF_FLOOR), aon_records: &J775D_AON_RECORDS, register_windows: &J775D_REGISTER_WINDOWS, + register_slot_flags: None, // The minimum parameter buffer for eight clusters. min_parameter_buffer: 0x0050_0000, // 16 MiB render blocks in a 16 GiB inventory. @@ -2267,6 +2274,7 @@ pub(crate) const J813: Board = Board { relative_frequency: relative_frequency(&J813_GPU_PERF_STATES, 1), aon_records: &J813_AON_RECORDS, register_windows: &J813_REGISTER_WINDOWS, + register_slot_flags: Some(&J813_HW_REGISTER_SLOT_FLAGS), min_parameter_buffer: (super::target::min_parameter_buffer_pages(1) << 15) as u32, // Half J714's: 2 MiB render blocks in a 2 GiB inventory. render_pool: (0x20_0000, 1 << 31), @@ -2275,9 +2283,11 @@ pub(crate) const J813: Board = Board { idle_standby_us: None, // One memory cache controller (/arm-io/sgx active-mcc-bit-vector; 25G76 +0xf8c = 1). mcc_mask: Some(0x1), - // The J813's 25G76 HW data has 6 at +0xf28 (J714's 12), common 26A428 +0x1044. topology_words: None, - hw_words: &[(0x1044, 6)], + // Words of macOS 27.0's own hardware data on this machine, read out of its bundle under + // aura, that the inherited 25G76 template and the generic encoding leave different. + hw_words: &[(0x0fa8, 0x8000), (0x0fb4, 2), (0x0fb8, 0), (0x1044, 0xc), (0x10a4, 0xa), + (0x10d4, 0x17), (0x1118, 0), (0x2660, 4), (0x2720, 1), (0x2724, 0)], fan_curve_start_mc: 84_000, g17d_forms: false, completion_inputs: false, @@ -2506,45 +2516,44 @@ const J714_REGISTER_WINDOWS: [M5RegisterWindowSpec; 30] = { /// (J873, same GPU base): slot 17 0x36000, DPEP (29) and ADCLK (30), and slot 32, the J813's /// 25G76 slot 27, which J714's list lacks. const J813_GPU_BASE: u64 = 0x5_8000_0000; -const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 32] = { +/// The J813's windows, read out of macOS 27.0's own hardware-data bundle under aura (its live +/// UAT maps exactly these, at exactly the device VAs [`m5_register_windows`] computes). +const J813_REGISTER_WINDOWS: [M5RegisterWindowSpec; 22] = { const G: u64 = J813_GPU_BASE; [ jw(0, &[0x3_8101_4000], 0x4000, 0, 2), jw(3, &[0x2_2010_4000], 0x18000, 0, 2), - jw(4, &[0x3_8106_8000], 0x4000, 0, 0), + jw(8, &[0x3_a135_0000], 0x4000, 0, 2), jw(9, &[0x3_803d_0000], 0x4000, 0, 2), jw(10, &[0x3_803c_0000], 0x4000, 0, 0), - jw(11, &[0x3_803d_8000], 0x4000, 0, 2), jw(12, &[0x4_2165_c000], 0x4000, 0, 2), jw(14, &[0x3_8028_0000], 0x8000, 0, 0), jw(15, &[0x3_8c84_0000], 0x24000, 0, 2), - // 25G76 maps 0x21400, the common 26A428 map 0x36000. - jw(17, &[G], 0x36000, 0, 2), - jw(18, &[G + 0xeb_4000], 0x4000, 0, 2), + // Ends right above the KSM block the firmware programs, as on 25G76. + jw(17, &[G], 0x21400, 0, 2), jw(22, &[G + 0x100_0000], 0x8000, 0, 2), - jw(23, &[G + 0xf0_0000], 0x10000, 0, 2), jw(24, &[G + 0xec_0000], 0x10000, 0, 2), - jw(29, &[G + 0x1800], 0x27f8, 0, 0), - jw(30, &[G + 0xec_2200], 0xfc, 0, 0), jw(31, &[G + 0xd0_4000], 0x8000, 0xd0_4000, 2), jw(32, &[G + 0xd0_d000], 0x1000, 0xd0_d000, 2), jw(33, &[G + 0xd5_0000], 0x10000, 0xd5_0000, 2), jw(34, &[G + 0xd1_0000], 0x4000, 0xd1_0000, 2), jw(36, &[G + 0xd4_0000], 0x4000, 0xd4_0000, 2), jw(37, &[G + 0xd6_0000], 0x8000, 0xd6_0000, 2), - jw(38, &[G + 0xd4_4000], 0x4000, 0xd4_4000, 2), - jw(39, &[G + 0xd4_c000], 0x200, 0xd4_c000, 2), jw(40, &[G + 0xe0_0000], 0x4000, 0xe0_0000, 2), - jw(42, &[G + 0xe4_0000], 0x4000, 0xe4_0000, 2), jw(44, &[G + 0xe0_8000, G + 0xe1_0000], 0x8000, 0, 2), jw(46, &[G + 0xe3_3800], 0x4000, 0, 2), - jw(48, &[G + 0xe6_0000], 0x58, 0xe6_0000, 2), - jw(52, &[G + 0xe4_8000], 0x8000, 0, 2), - jw(53, &[G + 0x2_3d00], 0x200, 0x2_3d00, 2), - jw(58, &[G + 0xe7_3800], 0x4000, 0, 2), + jw(47, &[G + 0xe3_5000], 0x4000, 0, 2), + jw(56, &[G + 0x1_4400], 0x400, 0x1_4400, 0), ] }; +/// The flag every one of the J813's slots carries, used or not, from macOS 27.0's own bundle; a +/// used slot's own flag agrees with it. +const J813_HW_REGISTER_SLOT_FLAGS: [u32; COMMON26_HW_REGISTER_MAP_SLOTS] = [ + 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 0, 0, 0, 0, 2, 0, 2, 2, 2, 2, 0, 0, 0, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 0, 0, 2, 0, +]; + /// J714 with all eight AMCCs in slot 3 (J716c amcc-enable-mask 0xff, MCC /// reg[14..21] + 0x104000) and four instances, one per cluster, in slots 20 and /// 24, where J714 has two at the same stride. Slot 44 is a fixed pair on every M5 @@ -2653,7 +2662,7 @@ impl Board { /// The board's register windows: the first [`Board::register_window_count`] entries. pub(crate) fn register_windows(&self) -> [M5RegisterWindow; M5_MAX_REGISTER_WINDOWS] { - m5_register_windows(self.register_windows) + m5_register_windows(self.register_windows, self.graph.is_common26()) } /// Register windows of the board. @@ -2662,7 +2671,7 @@ impl Board { } } -fn m5_register_windows(specs: &'static [M5RegisterWindowSpec]) +fn m5_register_windows(specs: &'static [M5RegisterWindowSpec], common26: bool) -> [M5RegisterWindow; M5_MAX_REGISTER_WINDOWS] { let mask = UAT_PAGE_SIZE as u64 - 1; @@ -2671,7 +2680,8 @@ fn m5_register_windows(specs: &'static [M5RegisterWindowSpec]) let mut next: u64 = 0; for (window, spec) in out.iter_mut().zip(specs) { let va = next + (spec.instances[0] & mask); - let span = spec.instances.len() as u64 * ((spec.size as u64 + mask) & !mask); + let size = if common26 { spec.size as u64 } else { (spec.size as u64 + mask) & !mask }; + let span = spec.instances.len() as u64 * size; *window = M5RegisterWindow { slot: spec.slot, device_va: HW_REGISTER_APERTURE.start + va, @@ -2680,7 +2690,11 @@ fn m5_register_windows(specs: &'static [M5RegisterWindowSpec]) unk_18: spec.unk_18, flag: spec.flag, }; - let end = (va + span + mask) & !mask; + // In the common 26A428 bundle the guard page follows the window's own bytes, before + // either is rounded up: a window whose size is not a whole number of pages does not push + // the next one past its page (macOS 27.0's J813 bundle, where slot 32's 0x1000 bytes + // leave the next window at the end of its own page). + let end = if common26 { va + span } else { (va + span + mask) & !mask }; next = (end + UAT_PAGE_SIZE as u64 + 0x7fff) & !0x7fff; } out @@ -2695,6 +2709,11 @@ pub(crate) fn encode_m5_register_map( return Err(InitdataError::BufferSize); } out.fill(0); + if let Some(flags) = board.register_slot_flags { + for (slot, flag) in flags.iter().enumerate() { + put_u32(out, slot * HW_REGISTER_MAP_ENTRY_SIZE + 0x20, *flag); + } + } for (index, window) in windows.iter().enumerate() { let spec = board.register_windows.get(index).ok_or(InitdataError::RegisterWindow)?; if window.slot >= COMMON26_HW_REGISTER_MAP_SLOTS From e4b2dba01cc578e835544b6ca85441866618a83e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 12:50:46 +0100 Subject: [PATCH 12/30] drm/asahi: g17: M5: publish the J813's rings where macOS 27.0 does The J813's GPU firmware died on an external abort every time it programmed the KSM completion-queue registers, with the completion table in its hardware data under the pointer that faulted. Read against macOS 27.0's own bundle, that table differed in one byte per ring: the GPU-visible alias of each completion ring is at 0x72_0184_8000 + n * 0x2_8000, not 0x70_... -- 8 GiB out, in the same top-level entry, inherited from the 25G76 trace this encoder was built from. The region views the same band holds move with them. Their firmware-side addresses were 0x18000 out as well: macOS keeps the four backings at the kernel window's 0xc07e_8000. Only the common 26A428 graph (the J813) moves; the M5 Pro's build keeps the 25G76 addresses. With the rings where the firmware expects them it no longer aborts: both coprocessors acknowledge their initialization data, the opening records retire and the driver registers a DRM device on the M5 Air for the first time. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 19 +++++++++++---- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 26 +++++++++++++++------ drivers/gpu/drm/asahi/g17/mod.rs | 1 + 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index f987b2298382a..805efdde2fb0c 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -96,6 +96,14 @@ 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::J714G17S26A428 | Self::J700G17P | Self::J813G17G25G76 => REGION_VIEW_LOW_ADDRS, + } + } + pub(crate) const fn repeated_region_bundle_offset(self) -> usize { match self { Self::J873G18G26A428 => COMMON26_SHARED_LAYOUT.block_88, @@ -208,6 +216,9 @@ pub(crate) const BUNDLE_VIEW_OFFSETS: [usize; 5] = [0x2740, 0x3380, 0x4400, 0xbc pub(crate) const BUNDLE_VIEW_EXTENTS: [usize; 5] = [0x18c0, 0x0c80, 0x3c00, 0x3380, 0x3300]; pub(crate) const REGION_VIEW_LOW_ADDRS: [u64; 2] = [0x70_0183_8000, 0x70_0184_0000]; +/// The GPU-visible aliases of the firmware's own tables on the J813: macOS 27.0 publishes them +/// 8 GiB above where the 25G76 trace had them (the J813's own completion table, read under aura). +pub(crate) const COMMON26_REGION_VIEW_LOW_ADDRS: [u64; 2] = [0x72_0183_8000, 0x72_0184_0000]; pub(crate) const CHANNEL_TABLE_ENTRIES: usize = 17; @@ -843,9 +854,9 @@ pub(crate) fn encode_main_config_for_profile( } put_u64(out, MAIN_REGION_VIEWS, views.sentinel_high); - put_u64(out, MAIN_REGION_VIEWS + 0x08, REGION_VIEW_LOW_ADDRS[0]); + put_u64(out, MAIN_REGION_VIEWS + 0x08, profile.region_view_low_addrs()[0]); put_u64(out, MAIN_REGION_VIEWS + 0x10, views.alias_high[0]); - put_u64(out, MAIN_REGION_VIEWS + 0x18, REGION_VIEW_LOW_ADDRS[1]); + put_u64(out, MAIN_REGION_VIEWS + 0x18, profile.region_view_low_addrs()[1]); put_u64(out, MAIN_REGION_VIEWS + 0x20, views.alias_high[1]); // J714's firmware takes both primary sentinels too; they are not // exclusive to the older J700 ABI. @@ -859,8 +870,8 @@ pub(crate) fn encode_main_config_for_profile( } (InstanceRole::Secondary, _) => { - put_u64(out, MAIN_REGION_VIEWS + 0x08, REGION_VIEW_LOW_ADDRS[0]); - put_u64(out, MAIN_REGION_VIEWS + 0x18, REGION_VIEW_LOW_ADDRS[1]); + put_u64(out, MAIN_REGION_VIEWS + 0x08, profile.region_view_low_addrs()[0]); + put_u64(out, MAIN_REGION_VIEWS + 0x18, profile.region_view_low_addrs()[1]); put_u32(out, MAIN_SECONDARY_KIND, match profile { RootProfile::J700G17P => MAIN_SECONDARY_KIND_VALUE, RootProfile::J813G17G25G76 => 0xc, diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index c5f0b7066e1a2..ef9527e6139c6 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -17,6 +17,10 @@ const KICK_SHARED: u64 = BASE + 0x1d_8000; const BUNDLE: u64 = BASE + 0xc078_8000; const COMPLETION_HIGH: u64 = BASE + 0xc080_0000; const COMPLETION_LOW: u64 = 0x70_0184_8000; +/// The completion rings, where macOS 27.0 keeps them on the J813 (its own UAT, read under aura: +/// four 0x20000 backings on this stride, the log buffers right above them). +const COMMON26_COMPLETION_HIGH: u64 = BASE + 0xc07e_8000; +const COMMON26_COMPLETION_LOW: u64 = 0x72_0184_8000; const COMPLETION_STRIDE: u64 = 0x2_8000; const PB: u64 = BASE + 0x15e_0000; const UMA: u64 = BASE + 0x15e_8000; @@ -29,12 +33,14 @@ impl InitData { /// The kernel lower-root ranges a bare-metal M5 firmware reaches, whose top-level links must /// exist before it starts: entry 1, the descriptor tables and completion rings (entry 7) and /// the first page of the canonical arena (`canonical`, entry 8), where queues go later. - pub(in crate::g17) fn m5_lower_links(canonical: core::ops::Range) + pub(in crate::g17) fn m5_lower_links(canonical: core::ops::Range, graph: &m::Graph) -> [core::ops::Range; 8] { let page = |address: u64| address..address + mmu::UAT_PGSZ as u64; - let ring = |index: u64| page(COMPLETION_LOW + index * COMPLETION_STRIDE); - [page(BOOT_LINK), page(m::REGION_VIEW_LOW_ADDRS[0]), page(m::REGION_VIEW_LOW_ADDRS[1]), + let low = if graph.is_common26() { COMMON26_COMPLETION_LOW } else { COMPLETION_LOW }; + let ring = |index: u64| page(low + index * COMPLETION_STRIDE); + let views = graph.profile.region_view_low_addrs(); + [page(BOOT_LINK), page(views[0]), page(views[1]), ring(0), ring(1), ring(2), ring(3), page(canonical.start)] } @@ -119,12 +125,18 @@ impl InitData { Placement::In(uat.kernel_dynamic_range(), mmu::UAT_PGSZ as u64), mmu::UAT_PGSZ, mmu::PROT_FW_SHARED_RW, CpuMap::WriteCombined)?)?; } - self.alias(self.pb, uat.kernel_lower_vm(), m::REGION_VIEW_LOW_ADDRS[0], GPU)?; - self.alias(self.page_pool, uat.kernel_lower_vm(), m::REGION_VIEW_LOW_ADDRS[1], GPU)?; + let views = board.graph.profile.region_view_low_addrs(); + self.alias(self.pb, uat.kernel_lower_vm(), views[0], GPU)?; + self.alias(self.page_pool, uat.kernel_lower_vm(), views[1], GPU)?; + let (low, high) = if common26 { + (COMMON26_COMPLETION_LOW, COMMON26_COMPLETION_HIGH) + } else { + (COMPLETION_LOW, COMPLETION_HIGH) + }; let mut completions = [m::G17PKsmCompletionAliases { low: 0, high: 0 }; m::M5_25G76_COMPLETION_QUEUE_COUNT]; for (index, pair) in completions.iter_mut().enumerate() { - pair.low = COMPLETION_LOW + index as u64 * COMPLETION_STRIDE; - pair.high = COMPLETION_HIGH + index as u64 * COMPLETION_STRIDE; + pair.low = low + index as u64 * COMPLETION_STRIDE; + pair.high = high + index as u64 * COMPLETION_STRIDE; // After initialization the host only reads completion records. // Match the standard G17 completion ring's CPU write-back mapping; // keep firmware/GPU aliases, size and lifetime unchanged. diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index b86a6965d1681..7b45d9a1dbb30 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1804,6 +1804,7 @@ impl Gpu { // link the tables of every lower range the M5 graph and its queues use first. uat.prepare_lower_tables(&initdata::InitData::m5_lower_links( uat.kernel_lower_dynamic_range(), + board.graph, )) .inspect_err(at("lower links"))?; // The J813 in native order, from aura's trace of macOS 27.0 on that machine: the start From 6c71c9c34b52350b56868a568962439fbc87ac71 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 12:50:46 +0100 Subject: [PATCH 13/30] drm/asahi: g17: M5: hand the J813's coprocessors their log buffers macOS 27.0 answers each coprocessor's request for the crash log buffer in its own data segment exactly as RTKit does here, by echoing the request -- and then sends a second message this driver never sent: on the crashlog endpoint, under a type of its own, a 32 KiB buffer of the host's at a fixed address in the firmware's kernel window. Without it the firmware asks for its buffer a second time, which is where this driver's mailbox history differed from macOS's. Both buffers are allocated and mapped with the rest of the graph, and published once both coprocessors run and before any endpoint of theirs is started, which is where macOS sends them -- GFX1's first, as with the initialization data. The firmware's history then matches macOS's message for message. The coprocessors are also started in macOS's order: GFX1's first, GFX's about 8 ms later. All of this is for the J813 only; the other boards keep their order and their preparation step. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 16 +++++++++ drivers/gpu/drm/asahi/g17/mod.rs | 43 ++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index ef9527e6139c6..166ff8111efc9 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -24,6 +24,10 @@ const COMMON26_COMPLETION_LOW: u64 = 0x72_0184_8000; const COMPLETION_STRIDE: u64 = 0x2_8000; const PB: u64 = BASE + 0x15e_0000; const UMA: u64 = BASE + 0x15e_8000; +/// The log buffer each J813 coprocessor is handed on the crashlog endpoint, by [`Role`]: the +/// addresses and the size macOS 27.0 publishes there. +const FW_LOG: [u64; 2] = [BASE + 0xc089_8000, BASE + 0xc088_8000]; +const FW_LOG_SIZE: usize = 0x8000; /// Lower-root entry 1 of the M5 layout (Mesa's driver window), linked before the firmware starts /// as the bare-metal J775D boot has always run. @@ -91,6 +95,11 @@ impl InitData { Err(EBUSY) } + /// The address and size of a coprocessor's log buffer, for the message that publishes it. + pub(in crate::g17) fn fw_log(role: Role) -> (u64, usize) { + (FW_LOG[role as usize], FW_LOG_SIZE) + } + pub(crate) fn build_m5(&mut self, dev: &AsahiDrmDevice, uat: &mmu::Uat, id: &crate::regs::G17Id, board: &m::Board) -> Result { use mmu::{PROT_FW_SHARED_RW as RW, PROT_FW_SHARED_RO as RO, @@ -116,6 +125,13 @@ impl InitData { self.add(fixed(FWCTL, mmu::UAT_PGSZ, RW)?)?; self.qos = self.add(fixed(QOS, m::G17P_QOS_RESOURCE_SIZE, RW)?)?; self.add(fixed(KICK_SHARED, m::M5_KICK_SHARED_SIZE, mmu::PROT_GPU_FW_SHARED_RW)?)?; + // The J813's log buffers, which the firmware is told about once it runs (see + // `publish_log_buffer`); it reads and writes them through its own kernel window. + if common26 { + for address in FW_LOG { + self.add(fixed_controller(address, FW_LOG_SIZE)?)?; + } + } let aon = self.add(fixed_controller(AON, m::M5_AON_PTD_SIZE)?)?; self.bundle = self.add(fixed_controller(BUNDLE, shared.total)?)?; self.pb = self.add(fixed(PB, mmu::UAT_PGSZ, RW)?)?; diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 7b45d9a1dbb30..e0c241c527022 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -284,12 +284,24 @@ pub(super) enum Role { Secondary = 1, } +/// RTKit's crash log endpoint, over which the host also publishes a log buffer of its own. +const EP_CRASHLOG: u8 = 0x01; /// RTKit endpoint of firmware messages: initialization, its acknowledgement and event /// notifications. const EP_FIRMWARE: u8 = 0x20; /// RTKit endpoint of host notifications to the firmware. const EP_DOORBELL: u8 = 0x21; +/// Host message on the crashlog endpoint that hands a coprocessor a log buffer of the host's +/// own, which it reaches through its kernel window: the size in 4 KiB pages and the buffer's +/// address, in the fields of a buffer request but under a type of its own. macOS 27.0 sends it +/// to both coprocessors once they run, after RTKit has echoed each one's request for the buffer +/// in its own data segment; without it the firmware asks for that buffer a second time. +const MSG_LOG_BUFFER: u64 = 0x9 << 52; +/// Bit position of [`MSG_LOG_BUFFER`]'s size, in 4 KiB pages. +const MSG_LOG_BUFFER_PAGES_SHIFT: u32 = 44; +/// Address bits of a buffer that [`MSG_LOG_BUFFER`] carries. +const MSG_LOG_BUFFER_ADDR_MASK: u64 = (1 << 44) - 1; /// Bit position of the type field of firmware messages. const MSG_TYPE_SHIFT: u32 = 48; /// Host message handing a coprocessor the address of its initialization root. @@ -623,6 +635,15 @@ impl Coprocessor { self.rtkit.send(EP_DOORBELL, msg) } + /// Hands the firmware the host's log buffer ([`MSG_LOG_BUFFER`]). + fn publish_log_buffer(&self, va: u64, size: usize) -> Result { + let msg = MSG_LOG_BUFFER + | (((size >> 12) as u64) << MSG_LOG_BUFFER_PAGES_SHIFT) + | (va & MSG_LOG_BUFFER_ADDR_MASK); + trace::message(trace::MAILBOX, msg); + self.rtkit.send(EP_CRASHLOG, msg) + } + /// Stops the CPU. The firmware keeps its state, but no longer runs. fn stop(&mut self) { if self.running { @@ -1893,9 +1914,15 @@ impl Gpu { let session = &mut **retained; // M5 renders take 64-byte metrics records. session.queues.metrics = metrics_ids; - session.primary.boot().inspect_err(at("primary boot"))?; - let secondary = session.secondary.as_mut().ok_or(EINVAL)?; - secondary.boot().inspect_err(at("secondary boot"))?; + // The J813's macOS 27.0 starts GFX1's coprocessor first and GFX's 330 ms later (aura's + // trace), the same way round as the InitData it publishes below. + if cfg.chip_id == 0x8142 { + session.secondary.as_mut().ok_or(EINVAL)?.boot().inspect_err(at("secondary boot"))?; + session.primary.boot().inspect_err(at("primary boot"))?; + } else { + session.primary.boot().inspect_err(at("primary boot"))?; + session.secondary.as_mut().ok_or(EINVAL)?.boot().inspect_err(at("secondary boot"))?; + } // Both coprocessors run: clear the J813's Fender gating here, where macOS 27.0 clears it // (it reads 0x6 and writes 0 after both ASCs are running and the first mailbox exchange). if cfg.chip_id == 0x8142 { @@ -1947,6 +1974,16 @@ impl Gpu { if unsafe { asahi_j813_pmp_report_agx(true) } != 0 { dev_warn!(dev.as_ref(), "M5: PMP AGX report failed\n"); } + // The J813's log buffers, GFX1's first and then GFX's, in macOS 27.0's order and + // before any endpoint of theirs is started. + for role in [Role::Secondary, Role::Primary] { + let (va, size) = initdata::InitData::fw_log(role); + let coprocessor = match role { + Role::Primary => &session.primary, + Role::Secondary => session.secondary.as_ref().ok_or(EINVAL)?, + }; + coprocessor.publish_log_buffer(va, size).inspect_err(at("log buffer"))?; + } } else { session .bridge From 65967c82de751380a13a121b4a9621b8ccbd342f Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 13:43:29 +0100 Subject: [PATCH 14/30] drm/asahi: g17: M5: the firmware's own offsets in the descriptor tail The first submission on the J813 killed the firmware every time: a data abort (ESR 0x96000004, a level-0 translation fault) on 0x20007, from 100c4: add x9, x8, #0xfb2 ; ldr x9, [x9] 100d0: cbz x9, done ; zero would have been fine 100d4: ldr w8, [x9, #0x8] ; -> the fault The firmware takes an eight-byte pointer at 0xfb2 of the compute descriptor, where this driver wrote `shared_control_va` at 0xfac: the load picked up that VA's top two bytes (0xffff) and the byte after it, so x9 was 0x1ffff and 0x1ffff + 8 is the faulting address. Six bytes out. 0xfac is not padding either -- the firmware reads a byte count there, rounds it up to eight, adds it to the shared-control object's own total and clears it (0x100f4..0x1010c) -- so the tail is: the pending count at 0xfac, two bytes, the pointer at 0xfb2. The usage object follows at 0xfcb, which is what the image reads; it never reads 0xfc5 or 0xfcd, the offsets this driver used. This is the common 26A428 interface's six-byte insert again, as in the render commands; the M5 Pro's build keeps the record at 0xfac. The descriptor now holds the tail as bytes and places the record by build. With the tail as the firmware reads it, the firmware survives the submission: the work runs instead of faulting. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/compute.rs | 35 +++++++++++++--------- drivers/gpu/drm/asahi/g17/queue/compute.rs | 1 + 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs b/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs index 1d969ffa42e8f..d290b5c9c580d 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs @@ -437,6 +437,8 @@ pub(crate) struct DescriptorArgs { pub(crate) event_metadata: [u8; 16], pub(crate) usage_fw_va: u64, pub(crate) completion_identity: u64, + /// The common 26A428 firmware's tail ([`Descriptor::tail`]). + pub(crate) common26_tail: bool, } /// The M5 descriptor has a shorter reserved region before shared-control @@ -473,22 +475,24 @@ pub(crate) struct Descriptor { status_va: [u64; 2], timestamp_va: [u64; 2], reserved_f9c: [u8; 0x10], - shared_control_va: u64, - shared_control_enabled: u8, - event_metadata: [u8; 16], - usage_fw_va: u64, - completion_identity: u64, - reserved_fd5: [u8; 0x2b], + /// From 0xfac: the shared-control pointer and its enable byte, the event metadata, the usage + /// object and the completion identity, packed in that order. The common 26A428 firmware (the + /// J813's) reads them [`COMMON26_TAIL_INSERT`] bytes later: it keeps a count at 0xfac of the + /// shared-control bytes it is yet to account for, which it rounds up to eight, adds to the + /// shared-control object's own total and clears (gfx 0x100c4..0x10110 use 0xfac, 0xfb2 and + /// 0xfcb there, never 0xfc5 or 0xfcd). + tail: [u8; DESCRIPTOR_PREFIX_BYTES - 0xfac], } +/// The bytes the common 26A428 firmware inserts before a compute descriptor's tail. +const COMMON26_TAIL_INSERT: usize = 6; + static_assert!(size_of::() == DESCRIPTOR_PREFIX_BYTES); static_assert!(core::mem::offset_of!(Descriptor, registers) == 0x40); static_assert!(core::mem::offset_of!(Descriptor, alternate) == 0x760); static_assert!(core::mem::offset_of!(Descriptor, stamp_va) == 0xf40); static_assert!(core::mem::offset_of!(Descriptor, completion_ordinal) == 0xf70); -static_assert!(core::mem::offset_of!(Descriptor, shared_control_va) == 0xfac); -static_assert!(core::mem::offset_of!(Descriptor, usage_fw_va) == 0xfc5); -static_assert!(core::mem::offset_of!(Descriptor, completion_identity) == 0xfcd); +static_assert!(core::mem::offset_of!(Descriptor, tail) == 0xfac); // SAFETY: Every field consists only of integer values and their arrays. unsafe impl Zeroable for Descriptor {} @@ -539,11 +543,14 @@ impl Descriptor { self.completion_ordinal = args.completion_ordinal; self.status_va = args.status_va; self.timestamp_va = args.timestamp_va; - self.shared_control_va = args.shared_control_va; - self.shared_control_enabled = u8::from(args.shared_control_va != 0); - self.event_metadata = args.event_metadata; - self.usage_fw_va = args.usage_fw_va; - self.completion_identity = args.completion_identity; + let mut at = if args.common26_tail { COMMON26_TAIL_INSERT } else { 0 }; + for field in [&args.shared_control_va.to_le_bytes()[..], + &[u8::from(args.shared_control_va != 0)], &args.event_metadata, + &args.usage_fw_va.to_le_bytes(), &args.completion_identity.to_le_bytes()] + { + self.tail[at..at + field.len()].copy_from_slice(field); + at += field.len(); + } Ok(()) } } diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 3e9d2002dafc3..dbbebb08cb628 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -2653,6 +2653,7 @@ impl Queue { dispatch_control: crate::g17::fw::m5::compute::dispatch_control(board, tuple.dispatch_base), auxiliary_control: tuple.auxiliary_control, }, + common26_tail: board.graph.is_common26(), descriptor_va: descriptor_low, // M5 Pro's descriptors carry the ordinal as the submission sequence too, as macOS's do. submit_sequence: if board.completion_inputs() { u64::from(self.ordinal) } else { 0 }, From 37198114c125a8e0c730fa0b107d0e189f8dabc6 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 14:04:30 +0100 Subject: [PATCH 15/30] drm/asahi: g17: M5: keep the J813's always-on block where macOS 27.0 does The policy names its always-on (AON) block through a pointer at bundle +0xb858. In macOS 27.0's own J813 bundle that pointer is the kernel window's 0xc07b_8000, right above the shared cluster; this driver put the block at 0x1d_0000 (the 25G76 trace's address, which the M5 Pro's build keeps), where macOS keeps an empty page -- which is why that page looked like an empty AON block in the first diff. Both name the same RT0g row. On its own this does not change what the firmware does with a submission (the GPU cores still stay off), but the block is now where the firmware has it on macOS. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index 166ff8111efc9..d405620c3e392 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -13,6 +13,10 @@ const REGION_A: u64 = BASE + 0x1b_8000; const FWCTL: u64 = BASE + 0x1c_0000; const QOS: u64 = BASE + 0x1c_8000; const AON: u64 = BASE + 0x1d_0000; +/// The always-on block the policy names, where macOS 27.0 keeps it on the J813: its policy's AON +/// pointer (bundle +0xb858, read under aura) is the kernel window's 0xc07b_8000, right above the +/// shared cluster. The 25G76 address, [`AON`], holds an empty page there. +const COMMON26_AON: u64 = BASE + 0xc07b_8000; const KICK_SHARED: u64 = BASE + 0x1d_8000; const BUNDLE: u64 = BASE + 0xc078_8000; const COMPLETION_HIGH: u64 = BASE + 0xc080_0000; @@ -132,7 +136,8 @@ impl InitData { self.add(fixed_controller(address, FW_LOG_SIZE)?)?; } } - let aon = self.add(fixed_controller(AON, m::M5_AON_PTD_SIZE)?)?; + let aon_va = if common26 { COMMON26_AON } else { AON }; + let aon = self.add(fixed_controller(aon_va, m::M5_AON_PTD_SIZE)?)?; self.bundle = self.add(fixed_controller(BUNDLE, shared.total)?)?; self.pb = self.add(fixed(PB, mmu::UAT_PGSZ, RW)?)?; self.page_pool = self.add(fixed(UMA, mmu::UAT_PGSZ, RW)?)?; @@ -216,7 +221,7 @@ impl InitData { &mut self.object_mut(self.bundle)?.bytes_mut()[..m::M5_HW_DATA_SIZE], enabled_cores) .map_err(|_| EINVAL)?; let bindings = m::M5PolicyBindings { - state: PRIVATE + private.policy_state as u64, aon_ptd: AON, boot_timestamp: 0, + state: PRIVATE + private.policy_state as u64, aon_ptd: aon_va, boot_timestamp: 0, }; if common26 { m::encode_common26_bundle_policy(board, bindings, self.object_mut(self.bundle)?.bytes_mut()) From 17d8a6e5b8577474563788459570183c2da4e3b9 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 14:04:30 +0100 Subject: [PATCH 16/30] drm/asahi: g17: M5: the J813's perf tables as macOS 27.0 has them Two differences from macOS 27.0's own J813 hardware data, in the per-state tables the firmware's power management reads: - The J813 has twelve states, but the padded fourteen-entry table repeats its top state as state 13, and every per-state table (frequency, scale, relative performance A and B, the core and SRAM voltage rows) carried that phantom state. macOS leaves state 13 zero. The tables now stop at the board's own state count; every other M5 board has thirteen states, so all fourteen rows as before. - Relative performance A (bundle +0x19e4) is an IEEE single per state in macOS's data -- 19.4 % at the lowest state up to 100 % -- where this encoder wrote the frequency ratio as a whole number (21, 30, ... 100), which read as floats are denormals. The values are not the frequency ratio, nor f*V or f*V^2: they are characterization data, so the J813 takes macOS's table and the other boards keep the ratio until theirs is read. With these the J813's perf area is macOS's byte for byte. It does not by itself get the GPU cores powered for a submission. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index 805efdde2fb0c..320544e7a5b79 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -2126,6 +2126,9 @@ pub(crate) struct Board { aux_perf_states: Option<&'static [M5AuxPerfState; 14]>, /// The relative-frequency curve of [`Board::perf_states`]. relative_frequency: [u32; 16], + /// Each state's relative performance in percent (IEEE single), as the board's own firmware + /// data has it; without one, the frequency ratio in whole percent. + relative_power: Option<&'static [u32; 14]>, /// Rows of the board's AON table the firmware reads. aon_records: &'static [[u32; 3]], /// Register windows the firmware maps, in device-VA order. @@ -2177,6 +2180,7 @@ pub(crate) const J714: Board = Board { perf_tables: 2, aux_perf_states: Some(&J714_AUX_PERF_STATES), relative_frequency: relative_frequency(&J714_GPU_PERF_STATES, 1), + relative_power: None, aon_records: &J714_AON_RECORDS, register_windows: &J714_REGISTER_WINDOWS, register_slot_flags: None, @@ -2206,6 +2210,7 @@ pub(crate) const J716C: Board = Board { perf_tables: 4, aux_perf_states: Some(&J716C_AUX_PERF_STATES), relative_frequency: relative_frequency(&J716C_GPU_PERF_STATES, 1), + relative_power: None, aon_records: &J716C_AON_RECORDS, register_windows: &J716C_REGISTER_WINDOWS, register_slot_flags: None, @@ -2242,6 +2247,7 @@ pub(crate) const J775D: Board = Board { perf_tables: 8, aux_perf_states: Some(&J775D_AUX_PERF_STATES), relative_frequency: relative_frequency(&J775D_GPU_PERF_STATES, J775D_PERF_FLOOR), + relative_power: None, aon_records: &J775D_AON_RECORDS, register_windows: &J775D_REGISTER_WINDOWS, register_slot_flags: None, @@ -2283,6 +2289,7 @@ pub(crate) const J813: Board = Board { perf_tables: 1, aux_perf_states: None, relative_frequency: relative_frequency(&J813_GPU_PERF_STATES, 1), + relative_power: Some(&J813_GPU_RELATIVE_POWER), aon_records: &J813_AON_RECORDS, register_windows: &J813_REGISTER_WINDOWS, register_slot_flags: Some(&J813_HW_REGISTER_SLOT_FLAGS), @@ -2391,6 +2398,14 @@ const fn one_table(states: [(u32, u32, u32); 14]) -> [M5PerfState; 14] { // J813 ADT sgx perf-states and perf-states-sram (MHz, mV): twelve states after the off state. // The encoder's table has fourteen; the last repeats the top state, which the HW data's count // of twelve leaves out. +/// The J813's per-state relative performance, read out of macOS 27.0's own hardware data under +/// aura (bundle +0x19e4): 19.4 % at the lowest state up to 100 % at the top, state 13 unused. +/// It is neither the frequency ratio nor f*V or f*V^2: characterization data. +const J813_GPU_RELATIVE_POWER: [u32; 14] = [ + 0, 0x419b_36eb, 0x41d0_6202, 0x4205_907a, 0x4227_4b32, 0x4247_eae1, 0x426b_0873, + 0x428a_5bac, 0x42a4_5b3f, 0x42ab_4981, 0x42b0_3cd5, 0x42bb_20f3, 0x42c8_0000, 0, +]; + pub(crate) const J813_GPU_PERF_STATES: [M5PerfState; 14] = one_table([ (0, 125, 770), (338, 660, 770), (486, 675, 770), (636, 690, 770), (796, 705, 770), (888, 735, 770), (988, 760, 770), (1084, 790, 790), @@ -2786,11 +2801,18 @@ pub(crate) fn encode_m5_hw_data( put_u32(g18, 0x283a, 1); put_u32(g18, 0x283e, 1); let states = board.perf_states; - for (state, &(frequency, core, sram)) in states.iter().enumerate() { + // Off and the board's `perf_state_count` states: the rows above them stay zero, as in macOS + // 27.0's J813 bundle, whose twelve states leave state 13 empty where the padded table repeats + // its top state. (Every other board has 13 states, so all fourteen rows.) + let used = board.perf_state_count as usize + 1; + for (state, &(frequency, core, sram)) in states.iter().enumerate().take(used) { put_u32(g18, COMMON26_HW_PERF_FREQUENCY + state * 4, frequency); put_u32(g18, COMMON26_HW_PERF_SCALE + state * 4, COMMON26_PERF_SCALE_VALUE); - let relative = frequency * 100 / states[13].0; + let relative = match board.relative_power { + Some(table) => table[state], + None => frequency * 100 / states[13].0, + }; put_u32(g18, COMMON26_HW_PERF_RELATIVE_A + state * 4, relative); for lane in 0..COMMON26_HW_PERF_LANES { let row = state * COMMON26_HW_PERF_ROW_STRIDE + lane * 4; @@ -2801,9 +2823,10 @@ pub(crate) fn encode_m5_hw_data( } for index in 0..COMMON26_HW_PERF_ROWS { put_u32(g18, COMMON26_HW_PERF_INDEX_MAP + index * 4, index as u32); - put_u32(g18, COMMON26_HW_PERF_RELATIVE_B + index * 4, board.relative_frequency[index]); + let relative = if index < used { board.relative_frequency[index] } else { 0 }; + put_u32(g18, COMMON26_HW_PERF_RELATIVE_B + index * 4, relative); if board.aux_perf_states.is_some() { - put_u32(g18, M5_HW_AUX_RELATIVE_FREQUENCY + index * 4, board.relative_frequency[index]); + put_u32(g18, M5_HW_AUX_RELATIVE_FREQUENCY + index * 4, relative); } } encode_m5_aux_perf_states(board, g18); From 2499e26e96aa6862408f550e34fa42d45555a1d4 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 14:37:08 +0100 Subject: [PATCH 17/30] drm/asahi: g17: M5: read back the firmware's log and hardware data Two debugfs files for comparing a running firmware with the one a machine's own operating system runs: - `firmware_log` dumps the log buffers the host publishes to each coprocessor, where an M5 firmware records what it is doing when it never reports a crash. (On the J813 they stay empty, as they do under macOS 27.0.) - `hardware_data` dumps the hardware-data bundle this driver built, so it can be diffed word for word against a bundle read out of a macOS guest. Reading the bundle through the UAT instead needs physdump, which cannot map the no-map page-table region while this driver holds it. Both take a torn snapshot of memory the firmware may be writing, so they are for diagnosis only. `KernelObject::bytes()` exists for the same reason. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/initdata.rs | 3 ++ drivers/gpu/drm/asahi/g17/initdata/m5.rs | 21 +++++++++- drivers/gpu/drm/asahi/g17/mod.rs | 22 ++++++----- drivers/gpu/drm/asahi/g17/object.rs | 7 ++++ drivers/gpu/drm/asahi/g17/reports.rs | 49 ++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 365ad7ada4b83..a61488a5ddce1 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -180,6 +180,8 @@ pub(super) struct InitData { _single_publisher: core::marker::PhantomData>, aliases: KVec, objects: KVec, + /// The log buffers published to each [`Role`], for reading back what the firmware wrote. + fw_log_objects: [Option; 2], roots: usize, cluster: usize, bundle: usize, @@ -291,6 +293,7 @@ impl InitData { _single_publisher: core::marker::PhantomData, aliases: KVec::new(), objects: KVec::new(), + fw_log_objects: [None; 2], roots: 0, cluster: 0, bundle: 0, diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index d405620c3e392..6de9d3e237ff3 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -104,6 +104,22 @@ impl InitData { (FW_LOG[role as usize], FW_LOG_SIZE) } + /// A coprocessor's log buffer, as the firmware has left it. Empty before the graph is built. + pub(in crate::g17) fn fw_log_bytes(&self, role: Role) -> &[u8] { + match self.fw_log_objects[role as usize].and_then(|index| self.object(index).ok()) { + Some(object) => object.bytes(), + None => &[], + } + } + + /// The hardware-data bundle as this driver built it, for diffing against a machine's own. + pub(in crate::g17) fn hardware_data_bytes(&self) -> &[u8] { + match self.object(self.bundle) { + Ok(object) => object.bytes(), + Err(_) => &[], + } + } + pub(crate) fn build_m5(&mut self, dev: &AsahiDrmDevice, uat: &mmu::Uat, id: &crate::regs::G17Id, board: &m::Board) -> Result { use mmu::{PROT_FW_SHARED_RW as RW, PROT_FW_SHARED_RO as RO, @@ -132,8 +148,9 @@ impl InitData { // The J813's log buffers, which the firmware is told about once it runs (see // `publish_log_buffer`); it reads and writes them through its own kernel window. if common26 { - for address in FW_LOG { - self.add(fixed_controller(address, FW_LOG_SIZE)?)?; + for (role, address) in FW_LOG.into_iter().enumerate() { + let object = self.add(fixed_controller(address, FW_LOG_SIZE)?)?; + self.fw_log_objects[role] = Some(object); } } let aon_va = if common26 { COMMON26_AON } else { AON }; diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index e0c241c527022..7991e99fff29c 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1468,6 +1468,8 @@ pub(crate) struct Gpu { _capacity: Pin>>, _reports: Pin>>, _power: Pin>>, + _firmware_log: Pin>>, + _hardware_data: Pin>>, } impl Gpu { @@ -1659,6 +1661,14 @@ impl Gpu { directory.read_only_file(c"power", reports::PowerView(shared.clone())), GFP_KERNEL, )?; + let firmware_log = KBox::pin_init( + directory.read_only_file(c"firmware_log", reports::FirmwareLogView(shared.clone())), + GFP_KERNEL, + )?; + let hardware_data = KBox::pin_init( + directory.read_only_file(c"hardware_data", reports::HardwareDataView(shared.clone())), + GFP_KERNEL, + )?; let stats = Arc::new( crate::stats::StatsSnapshot { g17_reader: Some(stats::Reader(shared.clone())), @@ -1678,6 +1688,8 @@ impl Gpu { _capacity: capacity, _reports: reports, _power: power, + _firmware_log: firmware_log, + _hardware_data: hardware_data, }, GFP_KERNEL, )?; @@ -1964,16 +1976,6 @@ impl Gpu { .inspect_err(at("root confirmation"))?; session.uat.mirror_secondary_root().inspect_err(at("secondary root"))?; if cfg.chip_id == 0x8142 { - // Bring-up trial: report AGX on through the PMP dashboard in native order (SGX - // start control and gating set, before InitData), as macOS does right after the - // platform power-on write. - extern "C" { - fn asahi_j813_pmp_report_agx(on: bool) -> core::ffi::c_int; - } - // SAFETY: the helper only ioremaps and pokes the mcc dashboard windows. - if unsafe { asahi_j813_pmp_report_agx(true) } != 0 { - dev_warn!(dev.as_ref(), "M5: PMP AGX report failed\n"); - } // The J813's log buffers, GFX1's first and then GFX's, in macOS 27.0's order and // before any endpoint of theirs is started. for role in [Role::Secondary, Role::Primary] { diff --git a/drivers/gpu/drm/asahi/g17/object.rs b/drivers/gpu/drm/asahi/g17/object.rs index 0b9854e84de2c..7395f04f34eee 100644 --- a/drivers/gpu/drm/asahi/g17/object.rs +++ b/drivers/gpu/drm/asahi/g17/object.rs @@ -253,6 +253,13 @@ impl KernelObject { self.size } + /// Reads the contents of the object. The firmware may be writing them, so a reader sees a + /// torn snapshot: for diagnostics, never for anything the host acts on. + pub(crate) fn bytes(&self) -> &[u8] { + // SAFETY: Per the type invariant `ptr` is valid for `size` bytes. + unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.size) } + } + /// Returns the contents of the object. /// /// Only for building an object the firmware does not access concurrently, before its address diff --git a/drivers/gpu/drm/asahi/g17/reports.rs b/drivers/gpu/drm/asahi/g17/reports.rs index a3bafd969981d..72771e5c4fe5f 100644 --- a/drivers/gpu/drm/asahi/g17/reports.rs +++ b/drivers/gpu/drm/asahi/g17/reports.rs @@ -132,6 +132,55 @@ impl kernel::debugfs::Writer for PowerView { } } +/// What each coprocessor has written into the log buffer the host published to it, which is +/// where an M5 firmware records what it is doing when it never reports a crash. +pub(super) struct FirmwareLogView(pub(super) kernel::sync::Arc); + +impl kernel::debugfs::Writer for FirmwareLogView { + fn write(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + let state = self.0.state.lock(); + let Some(firmware) = state.as_deref() else { + return writeln!(f, "online=0"); + }; + for role in [Role::Primary, Role::Secondary] { + let bytes = firmware.init.fw_log_bytes(role); + let used = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |last| last + 1); + writeln!(f, "role={} size={:#x} used={:#x}", role as u32, bytes.len(), used)?; + for (index, line) in bytes[..used].chunks(32).enumerate() { + write!(f, " {:#06x}:", index * 32)?; + for byte in line { + write!(f, " {:02x}", byte)?; + } + writeln!(f)?; + } + } + Ok(()) + } +} + +/// The hardware-data bundle this driver built, for comparing it word for word with the bundle a +/// machine's own firmware runs on. +pub(super) struct HardwareDataView(pub(super) kernel::sync::Arc); + +impl kernel::debugfs::Writer for HardwareDataView { + fn write(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + let state = self.0.state.lock(); + let Some(firmware) = state.as_deref() else { + return writeln!(f, "online=0"); + }; + let bytes = firmware.init.hardware_data_bytes(); + writeln!(f, "size={:#x}", bytes.len())?; + for (index, line) in bytes.chunks(32).enumerate() { + write!(f, "{:#06x}:", index * 32)?; + for byte in line { + write!(f, " {:02x}", byte)?; + } + writeln!(f)?; + } + Ok(()) + } +} + #[kernel::macros::kunit_tests(asahi_g17_reports)] mod tests { use super::*; From 16e524672a82d4d52f88fcbadbbdac3d7d9cc5d7 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 14:48:33 +0100 Subject: [PATCH 18/30] drm/asahi: g17: M5: hold the J813's GFX power domain for the session The GPU's power domain is gated by the time this driver probes: the boot loader leaves it on, genpd finds no active consumer and powers it off, and ps_gfx then reads target 0xf with actual 0 -- while the coprocessors still run, out of their own always-on island, and report the GPU busy. macOS 27.0 has 0x1f0023ff there. Two steps on the J813, as the G15 and G16 paths take for their own sessions: - `asahi_j813_hold_power()` takes the runtime-PM reference (`asahi_g15_hold_power`'s sequence), so nothing gates the domain under us. On its own it is not enough here: marking the device active does not request a transition, and this domain is already off by then. - `asahi_j813_power_on_gfx()` writes target 0xf with auto power management off and waits for the domain to reach it, then leaves it held. macOS re-enables auto power management because its own stack keeps a demand on the domain; this driver has nothing to keep one with yet. The domain now reads 0x0f0023ff, actual 0xf, for the whole session. The GPU's cores are a separate matter -- sgx+0xe01000 still reads 0, and a submission still does not retire -- but they cannot come up through a gated domain. This also drops the power-cycle trial from the boot path, which took the domain off and asked for it back with auto power management on, so it gated again as soon as the firmware went idle. That helper and the dashboard report helper, which no longer has a caller either, are removed. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/mod.rs | 30 ++++-- drivers/gpu/drm/asahi/g17/pmpdash.c | 144 ++++++++-------------------- 2 files changed, 62 insertions(+), 112 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 7991e99fff29c..ebe22e4f3f4b9 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1516,6 +1516,26 @@ impl Gpu { let m5 = cfg.profile.is_m5(); if m5 { let board = cfg.m5.ok_or(EINVAL)?; + // Hold the J813's GFX power domain for the session, as the G15 and G16 paths do + // for theirs. Without a hold genpd gates it once it finds no active consumer, and + // the GPU's cores can never come up: ps_gfx reads target 0xf with actual 0 while + // the coprocessors run out of their own always-on island and report the GPU busy. + if cfg.chip_id == 0x8142 { + extern "C" { + fn asahi_j813_hold_power(dev: *mut core::ffi::c_void) -> core::ffi::c_int; + fn asahi_j813_power_on_gfx() -> core::ffi::c_int; + } + // SAFETY: `dev` is this probe's device; runtime-PM accounting has no other + // requirement. + kernel::error::to_result(unsafe { + asahi_j813_hold_power(pdev.as_ref().as_raw().cast()) + }) + .inspect_err(|e| dev_err!(dev.as_ref(), "M5: no GFX power hold: {:?}\n", e))?; + // SAFETY: the helper only ioremaps and pokes the T8142 PMGR GPU power-state + // register. + kernel::error::to_result(unsafe { asahi_j813_power_on_gfx() }) + .inspect_err(|e| dev_err!(dev.as_ref(), "M5: GFX domain not powered: {:?}\n", e))?; + } // The M5 layouts are sized for every core slot of the board, fused-off cores // included, so the GPU is matched on clusters and slots, not on enabled cores. if id.num_clusters != board.clusters || id.num_clusters * id.num_cores != board.gpu_cores @@ -1846,16 +1866,6 @@ impl Gpu { // The other boards prepare both after the coprocessor boot (prepare_m5_firmware). if cfg.chip_id == 0x8142 { regs.set_m5_start_control().inspect_err(at("M5 start control"))?; - // Bring-up trial: force the ps_gfx transition macOS performs from off (the domain - // is inherited on here), then re-apply the SGX preparation the cycle resets. - extern "C" { - fn asahi_j813_pmp_agx_power_cycle() -> core::ffi::c_int; - } - // SAFETY: the helper only ioremaps and pokes the PMGR GPU power-state register. - if unsafe { asahi_j813_pmp_agx_power_cycle() } != 0 { - dev_warn!(dev.as_ref(), "M5: GFX power cycle incomplete\n"); - } - regs.set_m5_start_control().inspect_err(at("M5 start control after cycle"))?; } let primary = Coprocessor::new(pdev, dev, cfg, &uat, shared, Role::Primary) .inspect_err(|e| dev_err!(dev.as_ref(), "Primary coprocessor setup failed: {:?}\n", e))?; diff --git a/drivers/gpu/drm/asahi/g17/pmpdash.c b/drivers/gpu/drm/asahi/g17/pmpdash.c index acfe0fc916a0c..479e417bf2d40 100644 --- a/drivers/gpu/drm/asahi/g17/pmpdash.c +++ b/drivers/gpu/drm/asahi/g17/pmpdash.c @@ -1,16 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* - * Bring-up only: PMP helpers for the J813 GPU. - * - * report: report the AGX device state to the T8142 PMP over the mcc dashboard - * (SOC-DEV-PS-REQ idx 0x27, SOC-DEV-PS-ACK idx 0x2f), the way macOS ApplePMGR - * does in native order right after the platform power-on write. - * - * power_cycle: force the ps_gfx power transition macOS performs from off. The - * M5's domain is inherited on (0x1f0023ff), so Linux's genpd never transitions - * it; macOS writes target 0xf with auto-PM off, then auto-PM on, around the PMP - * report. A transition edge may be what makes the PMP enable the sgx node's - * power/clock gates 515 (GFX-SGX) and 516 (GFX-BUSY). + * GFX power for the J813 (T8142): hold the power domain and keep ps_gfx at its + * top state for the firmware session. */ #include @@ -18,19 +9,10 @@ #include #include #include +#include -int asahi_j813_pmp_report_agx(bool on); -int asahi_j813_pmp_agx_power_cycle(void); - -#define PTD_DB_BASE 0x3803c0000ULL -#define PTD_UPD_BASE 0x3803d0000ULL -#define PTD_WIN_SIZE 0x4000 - -#define PTD_PS_REQ 0x27 -#define PTD_PS_ACK 0x2f -#define PTD_META_FRESH BIT_ULL(1) - -#define AGX_BIT 12 +int asahi_j813_hold_power(struct device *dev); +int asahi_j813_power_on_gfx(void); #define SGX_PS_BASE 0x580e80000ULL #define SGX_PS_REG 0x110 @@ -45,65 +27,6 @@ int asahi_j813_pmp_agx_power_cycle(void); #define PS_AUTO_ENABLE BIT(28) #define PS_FLAGS (PS_WAS_PWRGATED | PS_WAS_CLKGATED) -static u64 ptd_read(void __iomem *db, u32 idx, u64 *meta) -{ - u64 v = readq(db + 16 * idx); - - *meta = readq(db + 16 * idx + 8); - return v; -} - -int asahi_j813_pmp_report_agx(bool on) -{ - u64 req, ack, meta, pre_ack, pre_meta, want; - ktime_t t0; - void __iomem *db, *upd; - bool fresh = false; - int ret = 0; - - db = ioremap(PTD_DB_BASE, PTD_WIN_SIZE); - upd = ioremap(PTD_UPD_BASE, PTD_WIN_SIZE); - if (!db || !upd) { - ret = -ENOMEM; - goto out; - } - - req = ptd_read(db, PTD_PS_REQ, &meta); - pre_ack = ptd_read(db, PTD_PS_ACK, &pre_meta); - want = on ? req | BIT_ULL(AGX_BIT) : req & ~BIT_ULL(AGX_BIT); - pr_info("asahi: PMP AGX %s: PS-REQ %#llx -> %#llx (pre-ACK %#llx meta %#llx)\n", - on ? "on" : "off", req, want, pre_ack, pre_meta); - writeq(want, upd + 8 * PTD_PS_REQ); - - t0 = ktime_get(); - for (;;) { - ack = ptd_read(db, PTD_PS_ACK, &meta); - if (meta & PTD_META_FRESH) - fresh = true; - if (!((ack ^ want) & BIT_ULL(AGX_BIT)) && - (fresh || ack != pre_ack)) { - pr_info("asahi: PMP AGX %s ACKed in %lld us (ACK %#llx meta %#llx fresh=%u)\n", - on ? "on" : "off", - ktime_us_delta(ktime_get(), t0), ack, meta, fresh); - break; - } - if (ktime_us_delta(ktime_get(), t0) > 15000000) { - pr_warn("asahi: PMP AGX %s NOT acked in 15 s (ACK %#llx meta %#llx fresh=%u)\n", - on ? "on" : "off", ack, meta, fresh); - ret = -ETIMEDOUT; - break; - } - usleep_range(100, 200); - } - -out: - if (upd) - iounmap(upd); - if (db) - iounmap(db); - return ret; -} - static u32 ptd_ps_write(void __iomem *ps, u32 pstate, bool auto_enable) { u32 cur = readl(ps); @@ -136,7 +59,37 @@ static int ptd_ps_wait(void __iomem *ps, u32 pstate, u32 *out) return 0; } -int asahi_j813_pmp_agx_power_cycle(void) +/* + * hold_power: keep the GFX power domain up for the firmware session, as the G15 + * and G16 paths do for theirs (asahi_g15_hold_power). Nothing held it on the + * J813: the domain is inherited on from the boot loader, genpd gates it when it + * finds no active consumer, and ps_gfx then reads target 0xf with actual 0 while + * the coprocessors run out of their own always-on island. Returns an error when + * the device has no power domain to hold. + */ +int asahi_j813_hold_power(struct device *dev) +{ + int ret; + + pm_runtime_get_noresume(dev); + ret = pm_runtime_set_active(dev); + if (ret) + return ret; + pm_runtime_enable(dev); + return dev->pm_domain ? 0 : -EINVAL; +} + +/* + * power_on: bring the GFX domain to its top power state and leave it there for + * the firmware session. The domain is gated by the time this driver probes + * (genpd finds no active consumer, and marking the device active does not + * request a transition), so ps_gfx reads target 0xf with actual 0 while the + * coprocessors run out of their own always-on island and the GPU's cores can + * never come up. Auto power management is left off, so nothing gates the + * domain again while the session holds it; macOS re-enables it because its own + * stack keeps a demand on the domain. + */ +int asahi_j813_power_on_gfx(void) { void __iomem *ps; u32 val, cur; @@ -145,32 +98,19 @@ int asahi_j813_pmp_agx_power_cycle(void) ps = ioremap(SGX_PS_BASE, 0x1000); if (!ps) return -ENOMEM; - cur = readl(ps + SGX_PS_REG); - pr_info("asahi: GFX ps before cycle: %#x\n", cur); - - val = ptd_ps_write(ps + SGX_PS_REG, 0, false); - if (ptd_ps_wait(ps + SGX_PS_REG, 0, &cur)) { - pr_warn("asahi: GFX ps did not power-gate (wrote %#x, now %#x)\n", - val, cur); - ret = -EIO; - } else { - pr_info("asahi: GFX ps powered off (wrote %#x, now %#x)\n", val, cur); + if (FIELD_GET(PS_ACTUAL, cur) == 0xf && !(cur & PS_AUTO_ENABLE)) { + pr_info("asahi: GFX ps already held on (%#x)\n", cur); + iounmap(ps); + return 0; } - val = ptd_ps_write(ps + SGX_PS_REG, 0xf, false); if (ptd_ps_wait(ps + SGX_PS_REG, 0xf, &cur)) { - pr_warn("asahi: GFX ps did not power on (wrote %#x, now %#x)\n", - val, cur); + pr_warn("asahi: GFX ps did not power on (wrote %#x, now %#x)\n", val, cur); ret = -EIO; } else { - pr_info("asahi: GFX ps powered on, auto-PM off (wrote %#x, now %#x)\n", - val, cur); + pr_info("asahi: GFX ps held on (wrote %#x, now %#x)\n", val, cur); } - - val = ptd_ps_write(ps + SGX_PS_REG, 0xf, true); - pr_info("asahi: GFX ps auto-PM on (wrote %#x)\n", val); - iounmap(ps); return ret; } From 12f0fbffb6eb1d56ceeedc4712d4199a9604504b Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 15:40:02 +0100 Subject: [PATCH 19/30] drm/asahi: g17: say what the firmware did with a render that timed out A render whose watchdog expires reaches its client as a lost device and the log as a recovery with no reason, which does not separate work the firmware never picked up from work it started and never finished. The evidence for that is already read on this path -- the pair's ring cursors, its pass record, its completion stamps -- and then thrown away. Print it instead, for the first few groups of a boot. On the J813 it says the firmware consumed every ring entry of a clear-only render and retired it with an untouched pass record: it ran nothing. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/queue/render.rs | 38 +++++++++++++++++++ .../drm/asahi/g17/runtime/render/settle.rs | 1 + 2 files changed, 39 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index ca504ed87db91..d083e73fa4cd3 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1547,6 +1547,44 @@ impl Pair { Ok(true) } + /// Evidence for a group whose watchdog expired: whether the firmware consumed its ring + /// entries (the cursors) and whether its halves ran (the pass record, which the tiler and + /// fragment processing write). It separates work the firmware never picked up from work it + /// started and never finished, which the error alone does not say. The first few groups of a + /// boot report; the rest would repeat them. + pub(crate) fn report_timeout(&self, stamps: [u32; 2], state: u32) { + static REPORTED: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + if REPORTED.fetch_add(1, core::sync::atomic::Ordering::Relaxed) >= 4 { + return; + } + let Some(front) = self.active.front() else { + return; + }; + let ordinal = front.ticket.ordinal; + let Ok(snapshot) = observation::snapshot(&self.memory, ordinal) else { + pr_warn!( + "G17: render timeout slot {} ordinal {}: its graph could not be read\n", + self.slot, + ordinal + ); + return; + }; + pr_warn!( + "G17: render timeout slot {} ordinal {} qids {:?}: stamps [{:#x}, {:#x}], recovery state {}, pass [{:#x}, {:#x}], tiler {:?}, fragment {:?}, job list {}\n", + self.slot, + ordinal, + self.qids(), + stamps[0], + stamps[1], + state, + snapshot.pass[0], + snapshot.pass[1], + snapshot.cursors[0], + snapshot.cursors[1], + if snapshot.job_list_empty { "empty" } else { "not empty" } + ); + } + fn selected(&self, masks: Option<[u64; 2]>) -> bool { masks.is_none_or(|masks| completion_noted(self.qids(), masks)) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/settle.rs b/drivers/gpu/drm/asahi/g17/runtime/render/settle.rs index 71f0bfc052696..7b13ccba87174 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/settle.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/settle.rs @@ -90,6 +90,7 @@ impl crate::g17::Firmware { .as_mut() .and_then(|entry| entry.pair.as_mut()) { + pair.report_timeout(stamps, state); pair.expire_stamp_wait(packet, stamps, state)?; } } From e5bf56008ef492869f9086101269238e4b0739de Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 15:40:02 +0100 Subject: [PATCH 20/30] drm/asahi: g17: name the command a refused submission was refused for A submission the driver rejects reaches userspace as EINVAL alone. Neither the command the parser stopped at nor the render that missed a requirement is named, so a userspace driver whose commands this kernel will not take has nothing to go on. Print the command's header where the parse stops, the counts where the walk ends short, and the render's own fields -- the ones the requirements are written in -- where validation fails. The first few refusals of a boot report; further ones would repeat them. The asahi-m4 host test compiles this parser (test-command-stream.py); its harness gets a pr_warn! that checks the format arguments. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/command.rs | 62 +++++++++++++++++++ .../gpu/asahi-m4/test-command-stream.py | 2 + 2 files changed, 64 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/command.rs b/drivers/gpu/drm/asahi/g17/command.rs index 796d072364fd2..54ad8992994c5 100644 --- a/drivers/gpu/drm/asahi/g17/command.rs +++ b/drivers/gpu/drm/asahi/g17/command.rs @@ -199,7 +199,34 @@ impl<'a> Parser<'a> { } } + /// A refused submission reaches userspace as EINVAL alone, which names neither the command + /// the parser stopped at nor what it holds. Name both, for the first few of a boot. pub(crate) fn next(&mut self) -> Result> { + let head = self.bytes; + let result = self.next_parsed(); + if let Err(error) = &result { + static REFUSED: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + if REFUSED.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 8 { + match Reader::new(head).read::() { + Ok(header) => pr_warn!( + "G17: refused a submission ({:?}) at its command of type {}, {} bytes, \ + barriers [{:#x}, {:#x}], with {} of the submission's bytes left\n", + error, header.cmd_type, header.size, header.vdm_barrier, + header.cdm_barrier, head.len() + ), + Err(_) => pr_warn!( + "G17: refused a submission ({:?}): its remaining {} bytes are not a \ + command header\n", + error, + head.len() + ), + } + } + } + result + } + + fn next_parsed(&mut self) -> Result> { while !self.bytes.is_empty() { let mut reader = Reader::new(self.bytes); let header: uapi::drm_asahi_cmd_header = reader.read()?; @@ -284,6 +311,13 @@ impl<'a> Parser<'a> { pub(crate) fn finish(&self) -> Result { if !self.bytes.is_empty() || self.counts == [0; 2] { + pr_warn!( + "G17: refused a submission: {} bytes past its last command, {} renders and {} \ + dispatches\n", + self.bytes.len(), + self.counts[0], + self.counts[1] + ); return Err(EINVAL); } Ok(()) @@ -494,11 +528,39 @@ impl Payload { } } + /// A refused submission reaches userspace as EINVAL alone, which names neither the command + /// nor the requirement it missed. Print the fields the requirements below are written in, + /// as userspace sent them, for the first few refusals of a boot. pub(crate) fn validate( &self, window: &UscWindow, variant: &super::fw::Variant, space: &impl AddressSpace, + ) -> Result { + let result = self.validate_checked(window, variant, space); + if let (Err(error), Self::Render(cmd, ..)) = (&result, self) { + static REFUSED: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + if REFUSED.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 4 { + pr_warn!( + "G17: refused a render ({:?}): {}x{} px, {} layers, {} samples, {} B/sample, \ + utile {}x{}, flags {:#x}, vdm {:#x}, scissor {:#x}, dbias {:#x}, oclqry {:#x}, \ + usc bg {:#x} eot {:#x} partial bg {:#x} eot {:#x}, samplers {} at {:#x}\n", + error, cmd.width_px, cmd.height_px, cmd.layers, cmd.samples, + cmd.sample_size_B, cmd.utile_width_px, cmd.utile_height_px, cmd.flags, + cmd.vdm_ctrl_stream_base, cmd.isp_scissor_base, cmd.isp_dbias_base, + cmd.isp_oclqry_base, cmd.bg.usc, cmd.eot.usc, cmd.partial_bg.usc, + cmd.partial_eot.usc, cmd.sampler_count, cmd.sampler_heap + ); + } + } + result + } + + fn validate_checked( + &self, + window: &UscWindow, + variant: &super::fw::Variant, + space: &impl AddressSpace, ) -> Result { // Everything M5 accepts beyond the G17P command set is gated on this. let m5 = variant.m5_commands; diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/test-command-stream.py b/tools/testing/selftests/drivers/gpu/asahi-m4/test-command-stream.py index b94595827a791..b0c92c63fd028 100755 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/test-command-stream.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/test-command-stream.py @@ -73,6 +73,8 @@ const E2BIG: i32 = -7; struct Error; impl Error { fn from_errno(error:i32)->i32 { error } } extern crate self as kernel; +// The parser logs why it refuses a command; check the format arguments, print nothing. +macro_rules! pr_warn { ($($arg:tt)*) => {{ let _ = format_args!($($arg)*); }}; } pub mod bindings { pub const EOPNOTSUPP:i32=95; } pub mod prelude { pub type Result = core::result::Result; pub const E2BIG:i32=-7; } mod uapi { include!("FULL_UAPI"); } From c52c64a47deec320bfaff6df26e4f75e5a12cadc Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 15:55:10 +0100 Subject: [PATCH 21/30] drm/asahi: g17: name the step a submission was refused at A command's own requirements are only one of the steps an ioctl's commands pass: the VM pin, the backend's preparation, the barrier and prior ordering, the tiling prefixes and the timestamp destinations each refuse a submission with the same bare EINVAL. Name the step. On the J813 this located the refusal of every render one userspace driver sends in the backend's preparation, which requires a render to declare its per-program storage. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/queue/mod.rs | 52 +++++++++++++++++++------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index e3be25d0ceb20..2b86aeff24f1e 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -433,7 +433,20 @@ impl Queue { bytes: &[u8], objects: Pin<&xarray::XArray>>, ) -> Result { - let parse_guard = self.context.vm().retain_first_job()?; + // A refused submission reaches userspace as EINVAL alone, so name the step that + // refused it: the command's own requirements are only one of several. + let refused = |step: &str, error: kernel::error::Error| { + static REFUSED: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + if REFUSED.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 8 { + pr_warn!("G17: refused a submission at {}: {:?}\n", step, error); + } + error + }; + let parse_guard = self + .context + .vm() + .retain_first_job() + .map_err(|error| refused("pinning the VM", error))?; let mut parser = Parser::new(bytes); // All historical ordinals use the same entry counters; publication // starts only after the entire ioctl has been validated and pinned. @@ -441,36 +454,47 @@ impl Queue { let mut preceding = [0; 2]; let mut pending = KVec::with_capacity(1, GFP_KERNEL)?; while let Some(command) = parser.next()? { - let mut payload = self.proof.validate( - self.context.vm(), - self.range.clone(), - &self.window, - self.variant, - &command.payload, - )?; + let mut payload = self + .proof + .validate( + self.context.vm(), + self.range.clone(), + &self.window, + self.variant, + &command.payload, + ) + .map_err(|error| refused("the command's requirements", error))?; let engine = match &payload { Validated::Render { .. } => Engine::Render, Validated::Compute { .. } => Engine::Compute, }; if !self.backend.supports_engine(engine) { return Err(ENOTSUPP); } - self.backend.prepare_command(&mut payload)?; + self.backend + .prepare_command(&mut payload) + .map_err(|error| refused("preparing the command", error))?; let mut order = Order::new( base, preceding, command.barriers, command.payload.prior(), engine, - )?; + ) + .map_err(|error| refused("its barriers and priors", error))?; if let Some((barriers, prior)) = command.payload.tiling_prefixes() { - order.narrow_tiling(&Order::new(base, preceding, barriers, prior, engine)?)?; + let tiling = Order::new(base, preceding, barriers, prior, engine) + .map_err(|error| refused("its tiling prefixes", error))?; + order + .narrow_tiling(&tiling) + .map_err(|error| refused("narrowing to its tiling prefixes", error))?; } preceding[engine.index()] += 1; let destinations = match &payload { - Validated::Render { timestamps, .. } => Destinations::resolve(objects, timestamps)?, + Validated::Render { timestamps, .. } => Destinations::resolve(objects, timestamps), Validated::Compute { timestamps, .. } => { - Destinations::resolve(objects, core::slice::from_ref(timestamps))? + Destinations::resolve(objects, core::slice::from_ref(timestamps)) } - }; + } + .map_err(|error| refused("its timestamp destinations", error))?; pending.push((payload, order, destinations), GFP_KERNEL)?; } parser.finish()?; From e6bb758687a8b749bc1da7bd836f467c8a2ee83d Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 8 Nov 2026 17:19:06 +0100 Subject: [PATCH 22/30] drm/asahi: g17: M5: the J813's render commands as its own kext writes them The common 26A428 interface inserts six bytes before the render command's UMA pointer, moving the whole trailing record. macOS 27.0's own AGXG17G (AGX-PI_300-G-A0, HAL300) is the ground truth: its AGXTAChannelSKSM::submitBuffer writes the tiling command at +0x934 (UMA state), +0x93c (the flag byte), +0x93d, +0x945, +0x94d, +0x975/+0x985 (the extra addresses) and +0x995 (their flags), and AGX3DChannelSKSM::submitBuffer the fragment one at +0x21ce, +0x21d6, +0x21d7, +0x21df, +0x21e7, +0x220f, +0x2213, +0x2214/+0x2224, +0x2234 and +0x2235 -- every one of them exactly six bytes above the offset this encoder, built for the M5 Pro, writes. The old 25G76 M5 encoder used the same offsets as the kext does now, which is why the pre-27.0 driver's renders were accepted. Move the record after encoding, for the common26 graph only, and grow the tiling command to 0x9a0 bytes to hold it. Compared field by field against both of the kext's writers, the J813's TA and 3D commands now agree with macOS's at every offset it writes. This is not by itself what keeps a render from executing: with it, the kernel and the firmware still retire a kick without running the work. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/render.rs | 2 +- .../drm/asahi/g17/fw/m5/render_assembly.rs | 5 ++ .../gpu/drm/asahi/g17/fw/m5/render_tail.rs | 57 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/render.rs b/drivers/gpu/drm/asahi/g17/fw/m5/render.rs index f53956102b610..86cada2de9c07 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/render.rs @@ -7,7 +7,7 @@ use super::super::kick::{KickTimestamp, RegisterWrite, RegisterArrayBinding}; use super::render_tail::{self as tail, RenderStage, RenderTail}; -pub(crate) const M5_TA_COMMAND_BYTES: usize = 0x990; +pub(crate) const M5_TA_COMMAND_BYTES: usize = 0x9a0; pub(crate) const M5_3D_COMMAND_BYTES: usize = 0x2240; pub(crate) const M5_TA_SELECTOR: u32 = 0; pub(crate) const M5_3D_SELECTOR: u32 = 1; diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/render_assembly.rs b/drivers/gpu/drm/asahi/g17/fw/m5/render_assembly.rs index 1966ab22f4878..7fc107313d096 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/render_assembly.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/render_assembly.rs @@ -70,6 +70,11 @@ pub(crate) fn encode_into(storage: &mut [u8], low: u64, ta.fill(0); fragment.fill(0); let fragment_rce = cmd::encode_m5_3d_command(fragment, fragment_low, inputs, &fragment_programs).map_err(|_| EINVAL)?; let ta_rce = cmd::encode_m5_ta_command(ta, low, inputs, &ta_program).map_err(|_| EINVAL)?; + if controls.board.graph.is_common26() { + use super::render_tail::{common26_layout, RenderStage}; + common26_layout(ta, RenderStage::Tiling).ok_or(EINVAL)?; + common26_layout(fragment, RenderStage::Fragment).ok_or(EINVAL)?; + } let (ta_storage, fragment_storage) = storage.split_at_mut(FRAGMENT_OFFSET); ta_storage[..cmd::M5_TA_COMMAND_BYTES].copy_from_slice(ta); fragment_storage[..cmd::M5_3D_COMMAND_BYTES].copy_from_slice(fragment); diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/render_tail.rs b/drivers/gpu/drm/asahi/g17/fw/m5/render_tail.rs index b67bce2fa60ce..81fcbc93b27e2 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/render_tail.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/render_tail.rs @@ -4,6 +4,21 @@ #[derive(Clone, Copy)] pub(crate) enum RenderStage { Tiling, Fragment } +/// The common 26A428 interface inserts six bytes before the UMA pointer. +/// Register programs and completion stamps precede the insert and keep their +/// offsets. Move the entire trailing record, including the context token and +/// extra-address list, after encoding the original M5 layout. +pub(crate) fn common26_layout(out: &mut [u8], stage: RenderStage) -> Option<()> { + let (start, end) = match stage { + RenderStage::Tiling => (0x92e, 0x990), + RenderStage::Fragment => (0x21c8, 0x2233), + }; + out.get(..end + 6)?; + out.copy_within(start..end, start + 6); + out[start..start + 6].fill(0); + Some(()) +} + /// A context token packs the six-bit execution identity above its reuse generation. /// The token in a render record agrees with the record header's context ID. pub(crate) fn context_token(context: u32, generation: u64) -> Option { @@ -68,3 +83,45 @@ pub(crate) fn encode_extra(out: &mut [u8], stage: RenderStage, value: &ExtraAddr if let RenderStage::Fragment = stage { tail[33..37].fill(0); } Some(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn common26_render_tail_offsets() { + for (stage, uma, token, extra, length) in [ + (RenderStage::Tiling, 0x934, 0x94d, 0x975, 0x9a0), + (RenderStage::Fragment, 0x21ce, 0x21e7, 0x2214, 0x2240), + ] { + let mut bytes = [0u8; 0x2240]; + let out = &mut bytes[..length]; + let pointer = 0xfffffc2000123400u64; + let identity = context_token(10, 1).unwrap(); + out[uma - 6..uma + 2].copy_from_slice(&pointer.to_le_bytes()); + out[uma - 7] = 0xa5; + encode(out, stage, &RenderTail { + descriptor_value: 0x1234, + resolved_value: Some(pointer), + trailing_value: identity, + }).unwrap(); + encode_extra(out, stage, &ExtraAddresses { + count: 1, policy: 2, addresses: [pointer; 4], + }).unwrap(); + common26_layout(out, stage).unwrap(); + assert_eq!(&out[uma..uma + 8], &pointer.to_le_bytes()); + assert_eq!(&out[token..token + 8], &identity.to_le_bytes()); + assert_eq!(&out[extra..extra + 8], &pointer.to_le_bytes()); + assert_eq!(out[extra + 32], 9); + assert_eq!(&out[uma - 6..uma], &[0; 6]); + assert_eq!(out[uma - 7], 0xa5); + } + } + + #[test] + fn common26_requires_room_for_extended_tiling_tail() { + let mut bytes = [0xa5; 0x990]; + assert!(common26_layout(&mut bytes, RenderStage::Tiling).is_none()); + assert!(bytes.iter().all(|&byte| byte == 0xa5)); + } +} From 4b06516de83b1e4fed6b8c3cc4345bb87fe6f1d8 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:54 +0100 Subject: [PATCH 23/30] drm/asahi: g17: M5: the J813's compute kicks as macOS 27.0 writes them macOS 27.0 on the J813 (common 26A428 firmware) kicks compute differently from the M5 Pro. Its WindowServer kicks, read under aura, show: - every dependency of every kick class (TA, 3D, compute) in class 1, two bits each at kick+0x180 (0x01, 0x05, 0x15, 0x55 for one to four); - the completion identity 0x0400000000000002 and the same 16 bytes of event metadata in every compute descriptor; - a completion ordinal that counts the queue's compute kicks (+0xf70 runs 0x341c, 0x341d, ...), as with the G17D forms; - the event mask bit 64 + context in word 1 and no entry gate; - the tuple preempt 0x1480 / dispatch base 0x14c08a60 / auxiliary control 0; - resource flags 0x0104054b: one secondary and one imageblock unit beside the primary one. Add Board::j813_forms() and take these forms on the J813 only; the J714, J716C and J775D keep theirs. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/compute.rs | 60 ++++++++++++++++----- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 6 +++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 14 +++-- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs b/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs index d290b5c9c580d..a2d7205dfe9af 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/compute.rs @@ -32,9 +32,17 @@ pub(crate) const COMPUTE_PIPELINED_DEPENDENCY: u8 = 2; /// The completion class, which holds a kick until the kick it names has completed. pub(crate) const COMPLETION_DEPENDENCY: u8 = 1; -/// Dependency class of an explicit dependency of a compute kick on `board`. +/// Dependency class of an explicit dependency of a compute kick on `board`: the completion class +/// where explicit waits take it ([`Board::completion_inputs`]) and on the J813, where macOS 27.0 +/// gives every dependency of every kick class 1, two bits each at kick+0x180 (0x01, 0x05, 0x15, +/// 0x55 for one to four), in its TA, 3D and compute kicks alike. Otherwise it is the pipelined +/// class. pub(crate) fn explicit_dependency(board: &Board) -> u8 { - if board.completion_inputs() { COMPLETION_DEPENDENCY } else { COMPUTE_PIPELINED_DEPENDENCY } + if board.completion_inputs() || board.j813_forms() { + COMPLETION_DEPENDENCY + } else { + COMPUTE_PIPELINED_DEPENDENCY + } } /// Dependency class of a compute kick's implicit parent (the previous kick of its queue) on @@ -53,10 +61,21 @@ pub(crate) fn parent_dependency(board: &Board, parent_retired: bool) -> u8 { } /// Whether compute kicks on `board` count their completion ordinal and hold for their parent: -/// with the G17D forms, and on boards whose explicit waits take the completion class -/// ([`Board::completion_inputs`]), as macOS encodes M5 Pro compute. +/// with the G17D forms, on boards whose explicit waits take the completion class +/// ([`Board::completion_inputs`]), as macOS encodes M5 Pro compute, and on the J813, whose +/// macOS 27.0 compute descriptors count it (+0xf70 runs 0x341c, 0x341d, ...). pub(crate) fn counted_forms(board: &Board) -> bool { - board.g17d_forms() || board.completion_inputs() + board.g17d_forms() || board.completion_inputs() || board.j813_forms() +} + +/// The descriptor's completion identity and event metadata on `board`. macOS 27.0 on the J813 +/// writes the same two values into every compute descriptor it kicks. +pub(crate) fn completion_forms(board: &Board) -> (u64, [u8; 16]) { + if board.j813_forms() { + (0x0400_0000_0000_0002, [0, 0, 0x55, 0x01, 0, 0, 0, 0, 0, 0, 0x85, 0, 0, 0, 0, 0]) + } else { + (1, [0; 16]) + } } /// The completion ordinal of a compute command on `board`. With the counted forms @@ -79,10 +98,18 @@ const ENTRY_GATE_EVENT: u32 = 254; /// The event mask of a compute kick of context `context_id` on `board`, published with its /// current-kick notification. With the G17D forms ([`Board::g17d_forms`]) the kick waits on its /// own context's pool event (64 x 2 + the context), which the firmware holds while it processes -/// that pool's dependencies, and on the entry-signal gate; a context of 64 or more is EINVAL. With -/// the other counted forms ([`counted_forms`]) it is the pool event alone. Otherwise it is the -/// fixed `[0, 0, 8, 0]` (event 131, context 3's pool event, without the gate). +/// that pool's dependencies, and on the entry-signal gate; a context of 64 or more is EINVAL. +/// On the J813 it is event 64 + the context, in word 1, without the gate, as macOS 27.0 kicks it +/// there (WindowServer's context 1 kicks with 0x2 in word 1 and nothing else). With the other +/// counted forms ([`counted_forms`]) it is the pool event alone. Otherwise it is the fixed +/// `[0, 0, 8, 0]` (event 131, context 3's pool event, without the gate). pub(crate) fn kick_event_mask(board: &Board, context_id: u32) -> Result<[u64; 4]> { + if board.j813_forms() { + if context_id >= POOL_EVENT_CONTEXTS { + return Err(EINVAL); + } + return Ok([0, 1u64 << context_id, 0, 0]); + } if !counted_forms(board) { return Ok([0, 0, 8, 0]); } @@ -116,13 +143,22 @@ pub(crate) const NORMAL_TUPLE: KickTuple = pub(crate) const QUALIFIED_TUPLE: KickTuple = KickTuple { preempt_state_offset: 0x1480, dispatch_base: 0x10c0_8860, auxiliary_control: 0 }; -/// The DRM compute tuple of `board` ([`Board::g17d_forms`]) for a kick whose parent takes class -/// `parent_class`. Where explicit waits take the completion class ([`Board::completion_inputs`]) -/// it is macOS's M5 Pro form: the J714 preemption offset with the qualified dispatch base, and the -/// auxiliary control set only when the kick's parent is pipelined. +/// The tuple macOS 27.0 kicks the J813's compute with: the qualified preemption-state offset and +/// auxiliary control with the normal dispatch base (0x1a4d0 = preempt + 0x1480, 0x1a458 = +/// 0x14c08a60, 0xa731 = 0 in every WindowServer compute descriptor). +pub(crate) const J813_TUPLE: KickTuple = + KickTuple { preempt_state_offset: 0x1480, dispatch_base: 0x14c0_8a60, auxiliary_control: 0 }; + +/// The DRM compute tuple of `board` ([`Board::g17d_forms`], [`Board::j813_forms`]) for a kick +/// whose parent takes class `parent_class`. Where explicit waits take the completion class +/// ([`Board::completion_inputs`]) it is macOS's M5 Pro form: the J714 preemption offset with the +/// qualified dispatch base, and the auxiliary control set only when the kick's parent is +/// pipelined. pub(crate) fn kick_tuple(board: &Board, parent_class: u8) -> KickTuple { if board.g17d_forms() { QUALIFIED_TUPLE + } else if board.j813_forms() { + J813_TUPLE } else if board.completion_inputs() { KickTuple { preempt_state_offset: NORMAL_TUPLE.preempt_state_offset, diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index 320544e7a5b79..e4de37f639579 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -2681,6 +2681,12 @@ impl Board { self.completion_inputs } + /// Whether this is the J813, whose register programs macOS 27.0 writes in the common 26A428 + /// forms rather than the M5 Pro's (read out of its own kicks under aura). + pub(crate) const fn j813_forms(&self) -> bool { + self.chip_id == 0x8142 + } + /// The board's `index`th register window. pub(crate) fn register_window_spec(&self, index: usize) -> Option<&'static M5RegisterWindowSpec> { self.register_windows.get(index) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index dbbebb08cb628..23af2363bd43b 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -2649,7 +2649,10 @@ impl Queue { // The board's core layout sizes the operand state, as on G17P. operand_region_bytes: self.graph.operand_state.bank_bytes(), operand_aux_offset: self.graph.operand_state.tail_offset(), - operand_bytes: self.graph.operand_state.size, resource_control: plan.map_or(0x54b, |p| p.resource_flags), + operand_bytes: self.graph.operand_state.size, resource_control: plan.map_or( + // macOS 27.0 on the J813 runs every compute program with one secondary and + // one imageblock unit beside the primary one (0x0104054b). + if board.j813_forms() { 0x0104_054b } else { 0x54b }, |p| p.resource_flags), dispatch_control: crate::g17::fw::m5::compute::dispatch_control(board, tuple.dispatch_base), auxiliary_control: tuple.auxiliary_control, }, @@ -2663,10 +2666,13 @@ impl Queue { kick_timestamp: kick_timestamp.get(), completion_ordinal: crate::g17::fw::m5::compute::completion_ordinal(board, self.ordinal), status_va: args.status_va, timestamp_va: [timestamp_va, timestamp_va + 8], - shared_control_va: self.pool.control_va(), event_metadata: [0;16], - usage_fw_va: self.graph.usage.gpu_va(), completion_identity: 1, + shared_control_va: self.pool.control_va(), + event_metadata: crate::g17::fw::m5::compute::completion_forms(board).1, + usage_fw_va: self.graph.usage.gpu_va(), + completion_identity: crate::g17::fw::m5::compute::completion_forms(board).0, }}); - // Explicit dependencies take the board's explicit class; the implicit parent, last, + // Explicit dependencies take the board's class + // ([`crate::g17::fw::m5::compute::explicit_dependency`]); the implicit parent, last, // takes its own ([`crate::g17::fw::m5::compute::parent_dependency`]). let mut m5_dependency_types = [m5_board.map_or( crate::g17::fw::m5::compute::COMPUTE_PIPELINED_DEPENDENCY, From b31f941e5904ed95fe8fb6e7cfbb79f473fbc503 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:54 +0100 Subject: [PATCH 24/30] drm/asahi: g17: M5: the J813's TA and fragment programs as macOS 27.0 writes them macOS 27.0's TA register program on the J813 keeps its own bracket: it opens with 0x1748 = 1 and closes with 0x1740 = 1. It carries the common registers unchanged, without the tiler parameter sub-buffer writes the M5 Pro program inserts. 0x10141 (copied to 0x1ca60) keeps the one-layer 0x200. Its fragment program writes no 0x160a8 before either tile write (90 registers instead of 92) and 0xa741 = 0 at entry. Build both programs in that form on the J813 only. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/buffer/target.rs | 2 +- .../drm/asahi/g17/fw/m5/render_registers.rs | 51 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer/target.rs b/drivers/gpu/drm/asahi/g17/buffer/target.rs index 8561053a336f2..8b87ccde7d985 100644 --- a/drivers/gpu/drm/asahi/g17/buffer/target.rs +++ b/drivers/gpu/drm/asahi/g17/buffer/target.rs @@ -277,7 +277,7 @@ impl Target { Ok(crate::g17::fw::m5::render_registers::TaExtra { gtp1: pointers[1], gtp2: pointers[2], gtp4: pointers[4], gtp5: pointers[5], gtp6: pointers[6], gtp7: pointers[7], segment_control: control[0], segment_index: control[1], - min_free_tvb_pages: control[2], scs_bracket: 0, matrix_bracket: 0, + min_free_tvb_pages: control[2], scs_bracket: 0, matrix_bracket: 0, j813: false, }) } diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/render_registers.rs b/drivers/gpu/drm/asahi/g17/fw/m5/render_registers.rs index 15e1887cee272..4bd3a7318b59a 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/render_registers.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/render_registers.rs @@ -327,16 +327,20 @@ pub(crate) struct TaExtra { pub(crate) min_free_tvb_pages: u64, pub(crate) scs_bracket: u8, pub(crate) matrix_bracket: u8, + /// The J813's form, as macOS 27.0 writes it there: the program keeps its own 0x1748/0x1740 + /// bracket (every TA program opens with 0x1748 = 1 and closes with 0x1740 = 1) and carries + /// the common registers unchanged, without the tiler parameter sub-buffer writes. + pub(crate) j813: bool, } pub(crate) struct TaProgram { - registers: [RegisterWrite; 97], + registers: [RegisterWrite; 99], length: usize, } impl TaProgram { pub(crate) const fn empty() -> Self { - Self { registers: [RegisterWrite {reg: 0, value: 0}; 97], length: 0 } + Self { registers: [RegisterWrite {reg: 0, value: 0}; 99], length: 0 } } pub(crate) fn registers(&self) -> &[RegisterWrite] { &self.registers[..self.length] } pub(crate) fn set_lifecycle(&mut self, value: u64) { @@ -361,10 +365,19 @@ pub(crate) fn tiling_with_scratch_into(common: &[RegisterWrite], e: TaExtra, if e.matrix_bracket & 1 != 0 { out.push(0x17f1, 1)?; } let mut deflake = false; for r in common.iter().copied() { - if r.reg == 0x1748 { continue; } + if r.reg == 0x1748 { + if e.j813 { out.push(0x1748, 1)?; } + continue; + } if r.reg == 0x1740 { if e.scs_bracket & 1 != 0 { out.push(0x17d9, 1)?; } if e.matrix_bracket & 1 != 0 { out.push(0x17e9, 1)?; } + if e.j813 { out.push(0x1740, 1)?; } + continue; + } + if e.j813 { + // 0x10141 (copied to 0x1ca60) keeps the one-layer 0x200, as macOS writes it. + out.push(r.reg, r.value)?; continue; } out.push(r.reg, r.value)?; @@ -387,8 +400,8 @@ pub(crate) fn tiling_with_scratch_into(common: &[RegisterWrite], e: TaExtra, _ => {} } } - if out.length != 92 + usize::from(e.scs_bracket & 1) * 2 + usize::from(e.matrix_bracket & 1) * 2 - + usize::from(mesh_storage != 0) { + if !e.j813 && out.length != 92 + usize::from(e.scs_bracket & 1) * 2 + + usize::from(e.matrix_bracket & 1) * 2 + usize::from(mesh_storage != 0) { return Err(EINVAL); } Ok(()) @@ -396,6 +409,14 @@ pub(crate) fn tiling_with_scratch_into(common: &[RegisterWrite], e: TaExtra, pub(crate) fn fragment_main_with_scratch_into(p: &RenderPass, common: &[RegisterWrite], dispatch: FragmentDispatch, descriptor_flag: u8, scs: bool, out: &mut [RegisterWrite]) -> Result { + fragment_main_forms_into(p, common, dispatch, descriptor_flag, scs, false, out) +} + +/// As [`fragment_main_with_scratch_into`]; with `j813`, the J813's macOS 27.0 form, which writes no +/// 0x160a8 before either tile write. +pub(crate) fn fragment_main_forms_into(p: &RenderPass, common: &[RegisterWrite], + dispatch: FragmentDispatch, descriptor_flag: u8, scs: bool, j813: bool, + out: &mut [RegisterWrite]) -> Result { let mut n = 0; let mut tile = 0; @@ -406,9 +427,11 @@ pub(crate) fn fragment_main_with_scratch_into(p: &RenderPass, common: &[Register } if r.reg == 0x16068 { if tile == 2 { return Err(EINVAL); } - out[n] = RegisterWrite { reg: 0x160a8, - value: if tile == 0 { dispatch.initial(descriptor_flag) } else { 0x20 } }; - n += 1; + if !j813 { + out[n] = RegisterWrite { reg: 0x160a8, + value: if tile == 0 { dispatch.initial(descriptor_flag) } else { 0x20 } }; + n += 1; + } tile += 1; } let slot = out.get_mut(n).ok_or(EINVAL)?; @@ -522,14 +545,20 @@ impl Workspace { build_ta_registers_into(p, &c, r.free_list_slot, ta, &self.common.ta)?; let mut extra = extra; extra.scs_bracket |= u8::from(c.ta.scs.scratch.enabled()); + extra.j813 = c.board.j813_forms(); tiling_with_scratch_into(ta, extra, r.mesh_storage, &mut self.ta)?; self.ta.set_lifecycle((u64::from(inputs.ta.predecessor_id) << 32) | u64::from(inputs.ta.completion.key as u32)); c.fragment_tile_flags = (c.fragment_tile_flags & !0x7ffff) | 0x36011 | (u64::from(inputs.fragment.empty_tile_flags[3] & 1) << 18); + let j813 = c.board.j813_forms(); + if j813 { + // macOS 27.0's J813 fragment program writes 0xa741 = 0 at entry. + c.fragment_event_control = 0; + } build_fragment_registers_into(p, &c, r.free_list_slot, &mut self.base, &self.common.fragment)?; - self.fragment_count = 92 + usize::from(c.fragment.scs.scratch.enabled()) * 2; - fragment_main_with_scratch_into(p, &self.base, dispatch, inputs.fragment.empty_tile_flags[3], - c.fragment.scs.scratch.enabled(), &mut self.fragment[..self.fragment_count])?; + self.fragment_count = if j813 { 90 } else { 92 } + usize::from(c.fragment.scs.scratch.enabled()) * 2; + fragment_main_forms_into(p, &self.base, dispatch, inputs.fragment.empty_tile_flags[3], + c.fragment.scs.scratch.enabled(), j813, &mut self.fragment[..self.fragment_count])?; for reg in &mut self.fragment { if matches!(reg.reg, 0x160e0 | 0x1499 | 0xa341) { reg.value = (u64::from(inputs.fragment.predecessor_id) << 32) | u64::from(inputs.fragment.completion.key as u32); From 838bbe844baef00278e764fa97536fcbe939796e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:55 +0100 Subject: [PATCH 25/30] drm/asahi: g17: start the work-state words at generation 1 macOS 27.0 never uses generation 0 for a context's work-state words: on the J813 they run 0x17c8.. 0x17ea. Start the sequence at 0x40, the first word of generation 1. Only the words' upper bits change; the nodes they address and the order they are drawn in are the same. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/context/identity.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/context/identity.rs b/drivers/gpu/drm/asahi/g17/context/identity.rs index 0fe045d6f780c..e8e57cf3ad185 100644 --- a/drivers/gpu/drm/asahi/g17/context/identity.rs +++ b/drivers/gpu/drm/asahi/g17/context/identity.rs @@ -36,7 +36,9 @@ pub(super) struct WorkStates { impl WorkStates { pub(super) const fn new() -> Self { Self { - next: AtomicU32::new(0), + // Start at generation 1: macOS 27.0 never uses generation 0 (its words run + // 0x17c8.. 0x17ea on the J813). + next: AtomicU32::new(0x40), live: [const { AtomicU32::new(FREE) }; 64], held: [const { AtomicU32::new(0) }; ENGINES], } From 752b3e44ef53b9e567ad2890091e7f5a08636ce1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:55 +0100 Subject: [PATCH 26/30] drm/asahi: g17: M5: submit to the J813 in the common 26A428 forms The J813's firmware (common 26A428) takes work in the G17P/Neo forms, not the M5 Pro's. macOS 27.0 there: - submits through the mailbox doorbell alone. It never writes the host port (sgx+0x4090/0x4098) or the KSM queue registers (sgx+0x21068..0x21080), and rings render doorbells as 0x83 | priority << 2, without the M5 Pro's bit 1; - fills its TA, 3D and compute item rings with [configuration], descriptor, KickAnnounce (tag 0xe) records, never the descriptor-less tag-16 notification; - sends a queue configuration with every kick and never publishes one alone. Leaving it out of a kick whose configuration had not changed let a fragment run under another client's (or no) context under parallel load; - names the paired 3D queue at +0x82 of the TA configuration, where the M5 Pro has the parameter manager; - names no event in its TA and 3D kicks. Do the same on the J813, gated by J813_FORMS, which the device sets from its board at probe. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/mod.rs | 10 +++++++++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 7 ++++++- drivers/gpu/drm/asahi/g17/queue/render.rs | 21 ++++++++++++++----- .../drm/asahi/g17/queue/render/publication.rs | 11 ++++++++-- drivers/gpu/drm/asahi/g17/runtime/render.rs | 2 +- drivers/gpu/drm/asahi/regs.rs | 3 +++ 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index ebe22e4f3f4b9..42745e4a8cba7 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -318,6 +318,15 @@ const MSG_INITDATA_DONE: u64 = 0x89 << MSG_TYPE_SHIFT; const MSG_EVENTS: u64 = 0x42 << MSG_TYPE_SHIFT; /// Primary processor notification for newly published device-control records. const MSG_CONTROL_NOTIFY: u64 = (0x84 << MSG_TYPE_SHIFT) | 0x11; + +/// The board is the J813 ([`fw::m5::initdata::Board::j813_forms`]), whose firmware (common +/// 26A428) takes work in the G17P forms. macOS 27.0 there submits through the doorbell alone: it +/// never writes the M5 Pro's host port (sgx+0x4090/0x4098) or the KSM queue registers +/// (sgx+0x21068..0x21080), and rings render doorbells as 0x83 | priority << 2 (0 and 8 in its +/// trace; compute is | 2). +pub(crate) static J813_FORMS: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + /// Host message, sent to both coprocessors once both are initialized, ending the power-on /// transition. The firmware then retires the opening device-control records. const MSG_POWER_ON_DONE: u64 = 0x89 << MSG_TYPE_SHIFT; @@ -1516,6 +1525,7 @@ impl Gpu { let m5 = cfg.profile.is_m5(); if m5 { let board = cfg.m5.ok_or(EINVAL)?; + J813_FORMS.store(board.j813_forms(), core::sync::atomic::Ordering::Relaxed); // Hold the J813's GFX power domain for the session, as the G15 and G16 paths do // for theirs. Without a hold genpd gates it once it finds no active consumer, and // the GPU's cores can never come up: ps_gfx reads target 0xf with actual 0 while diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 23af2363bd43b..75252dfe4f03d 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -2794,6 +2794,11 @@ impl Queue { })?; visible = true; self.unpublished_binding = false; + // The J813 (common 26A428) reads the descriptor from the item ring, after + // the configuration, as macOS 27.0's compute ring has it. + if crate::g17::J813_FORMS.load(Ordering::Relaxed) { + producer = self.graph.append(producer, descriptor_high, || {})?; + } // This Active and its Packet pins now own all visible records. if let Some(configuration) = self.configuration.take() { configuration.release(); } } else if first { @@ -2881,7 +2886,7 @@ impl Queue { } prepared_kick.commit(); self.submitted = self.submitted.checked_add(1).ok_or(EOVERFLOW)?; - let announce_va = if self.graph.m5 { + let announce_va = if self.graph.m5 && !crate::g17::J813_FORMS.load(Ordering::Relaxed) { self.graph.record(record_slot, ANNOUNCE, 0x40, crate::g17::fw::m5::queue::notification(qid, kick_timestamp)?)? } else { self.graph.record(record_slot, ANNOUNCE, 0x40, announce)? }; diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index d083e73fa4cd3..03dacf1734f51 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -839,7 +839,11 @@ impl Pair { crate::g17::fw::m5::queue::ConfigProfile { soft_fault_enabled: true, extended: false, - parameter_manager: (stage == 0).then(|| u32::from(self.buffer_id())), + // macOS 27.0's J813 TA configuration names the paired 3D queue at + // +0x82 (qid 1 beside pb_slot 0), not the parameter manager. + parameter_manager: (stage == 0 + && !crate::g17::J813_FORMS.load(Ordering::Relaxed)) + .then(|| u32::from(self.buffer_id())), }, )? } else { @@ -847,7 +851,10 @@ impl Pair { }); } let configs = [configs[0].ok_or(EIO)?, configs[1].ok_or(EIO)?]; - if m5 && self.registered.is_none() { + // The J813's (common 26A428) items carry configuration, descriptor and announcement in + // one publication; macOS 27.0 never publishes a configuration alone there. + let j813 = crate::g17::J813_FORMS.load(Ordering::Relaxed); + if m5 && !j813 && self.registered.is_none() { if self.installed == [true; 2] { // The cold-install configuration pair must be consumed first. let cursors = self.memory.graph.cursors()?; @@ -883,7 +890,10 @@ impl Pair { config.completion_seed_valid = 0; config.completion_seed = 0; } - publication::required(cache[stage], config) + // macOS 27.0 sends a configuration with every J813 kick, so the firmware always + // runs a kick under the context its own record names; leaving it out let a fragment + // run under another client's (or no) context under parallel load. + j813 || publication::required(cache[stage], config) }); let cursors = self.memory.graph.cursors()?; if cursors[0].write != self.cursors[0] || cursors[1].write != self.cursors[1] { @@ -941,8 +951,9 @@ impl Pair { }, }, mcache, - // M5 tiling kicks also retain the last event of the mask. - event_mask: if m5 && stage == 0 { + // M5 tiling kicks also retain the last event of the mask, except on the J813, whose + // macOS 27.0 TA and 3D kicks name no event at all. + event_mask: if m5 && stage == 0 && !j813 { [0, 0, 0, 1u64 << 62] } else { abi::RENDER_KICK_EVENT_MASK diff --git a/drivers/gpu/drm/asahi/g17/queue/render/publication.rs b/drivers/gpu/drm/asahi/g17/queue/render/publication.rs index 838d3b17bab86..9ee23de892401 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/publication.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/publication.rs @@ -62,7 +62,11 @@ impl<'a> Lane<'a> { timestamp: KickTimestamp, lead: bool, ) -> Result { - let m5 = graph.is_m5(); + // macOS 27.0 on the J813 (common 26A428 interface) publishes the Neo form: its + // TA, 3D and compute item rings carry [configuration], descriptor, KickAnnounce (tag 0xe) + // records, never the M5 Pro's descriptor-less tag-16 notification. + let j813 = crate::g17::J813_FORMS.load(core::sync::atomic::Ordering::Relaxed); + let m5 = graph.is_m5() && !j813; // M5 items carry no descriptor: [configuration], notification. let count = u32::from(present) + u32::from(lead) + if m5 { 1 } else { 2 }; let window = Window::new(base, count, ITEM_RING_ENTRIES)?; @@ -100,10 +104,13 @@ impl<'a> Lane<'a> { if lead { push(graph.config.gpu_va() + (config_offset + memory::LEAD) as u64); } + if j813 && present { + push(graph.config.gpu_va() + config_offset as u64); + } if !m5 { push(descriptor); } - if present { + if present && !j813 { push(graph.config.gpu_va() + config_offset as u64); } push(announce_va); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 4b62d96d7cd3c..7078367e7fcf9 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -706,7 +706,7 @@ impl crate::g17::queue::render::Host for Host<'_> { let result = self.firmware.primary.notify( (0x83 << crate::g17::MSG_TYPE_SHIFT) | u64::from(priority) << 2 - | if m5 { 2 } else { 0 }, + | if m5 && !crate::g17::J813_FORMS.load(core::sync::atomic::Ordering::Relaxed) { 2 } else { 0 }, ); if result.is_err() { self.firmware.primary.state.shared.lose_device_quietly(); diff --git a/drivers/gpu/drm/asahi/regs.rs b/drivers/gpu/drm/asahi/regs.rs index c40c5d448f3f4..84679b3b3609b 100644 --- a/drivers/gpu/drm/asahi/regs.rs +++ b/drivers/gpu/drm/asahi/regs.rs @@ -631,6 +631,9 @@ impl G17Resources { &self, submission: &crate::g17::fw::m5::queue::HardwareSubmission, ) -> Result { + if crate::g17::J813_FORMS.load(core::sync::atomic::Ordering::Relaxed) { + return Ok(()); + } let sgx = self.sgx.try_access().ok_or(ENODEV)?; let sgx = (&*sgx).as_view(); for &(offset, value) in submission.writes() { From 0baa7d332e1208c363ba58e7b2c37ed622b3a01b Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:55 +0100 Subject: [PATCH 27/30] drm/asahi: g17: M5: the J813's pools in the G17P form The J813 (common 26A428) keeps its private-memory pools in the G17P form. This was read from macOS 27.0's own objects on that machine: - The page-pool rows are the M5 descriptor's first four words on a 0x20 stride: [directory >> 7, control + 0x20, pages | entries << 32, 0] (macOS's table at firmware 0x20015f0000, slots 0, 1, 2). The M5 Pro form is eight words on 0x40. Written in that form, every pool the driver installed read as an empty row, and the firmware recovered within milliseconds of each kick, blaming the queue. - The pool control is the 0x68-byte G17P one: +0x00 render 1 / compute 3, +0x10 1, run cursor +0x48 (eight qwords per block), state +0x4c, completed +0x54, +0x5c compute 1, class +0x60 (render 2, compute 1). The M5 Pro control is the same with 0x20 bytes inserted at +0x38. - The run list has 0x40-byte slots of 512-page runs (the non-G17S geometry). - Only the first four saved descriptor fields live in the row; the rest are the pool's own. With this the J813 executes GPU work: compute fills and copies, renders clear and draw, and Vulkan and GL run. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/freelist.rs | 10 ++-- drivers/gpu/drm/asahi/g17/fw/m5/pool.rs | 50 +++++++++++++++++-- drivers/gpu/drm/asahi/g17/initdata.rs | 11 ++++ .../gpu/drm/asahi/g17/runtime/render/pools.rs | 21 ++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/freelist.rs b/drivers/gpu/drm/asahi/g17/freelist.rs index 3d1449bee2147..fde2759a63f4a 100644 --- a/drivers/gpu/drm/asahi/g17/freelist.rs +++ b/drivers/gpu/drm/asahi/g17/freelist.rs @@ -839,7 +839,8 @@ impl RenderPool { let mut bytes = [0; super::fw::m5::pool::STATE_BYTES]; super::fw::m5::pool::encode_g17s_render_state(pages.m5_layout(self.id())?, self.state.gpu_va(), &mut bytes).map_err(|_| EINVAL)?; - bytes[super::fw::m5::pool::COMPLETED..super::fw::m5::pool::COMPLETED + 8] + let completed_at = super::fw::m5::pool::completed_offset(); + bytes[completed_at..completed_at + 8] .copy_from_slice(&submitted.to_le_bytes()); let raw = self.control.pointer(0, bytes.len())?; for (index, byte) in bytes.into_iter().enumerate() { @@ -891,7 +892,7 @@ impl RenderPool { /// counter, so retain the byte accesses required by that interface. pub(crate) fn counters(&self) -> Result<[u64; 2]> { let submitted = self.state.dword(0)?.load(Ordering::Relaxed); - let offset = if self.is_m5() { super::fw::m5::pool::COMPLETED } + let offset = if self.is_m5() { super::fw::m5::pool::completed_offset() } else { FreeListControl::COMPLETED }; let completed = self.control.pointer(offset, 8)?; let mut bytes = [0; 8]; @@ -1073,7 +1074,8 @@ impl ComputePool { use super::fw::m5::pool; pool::encode_g17s_compute_state(pages.m5_layout(id)?, submitted, &mut control.bytes_mut()[..pool::STATE_BYTES]).map_err(|_| EINVAL)?; - control.bytes_mut()[pool::COMPLETED..pool::COMPLETED + 8].copy_from_slice(&completed); + let completed_at = pool::completed_offset(); + control.bytes_mut()[completed_at..completed_at + 8].copy_from_slice(&completed); Ok(()) } PageLayout::Neo { blocks, .. } => { @@ -1094,7 +1096,7 @@ impl ComputePool { Ok(u64::from_le_bytes(self.completed_bytes()?)) } fn completed_bytes(&self) -> Result<[u8; 8]> { - let offset = if self.m5 { super::fw::m5::pool::COMPLETED } else { FreeListControl::COMPLETED }; + let offset = if self.m5 { super::fw::m5::pool::completed_offset() } else { FreeListControl::COMPLETED }; let pointer = self.control.pointer(offset, 8)?; let mut bytes = [0; 8]; for (index, byte) in bytes.iter_mut().enumerate() { diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/pool.rs b/drivers/gpu/drm/asahi/g17/fw/m5/pool.rs index b8c52bc634c5f..7a457322fb64e 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/pool.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/pool.rs @@ -2,7 +2,7 @@ //! M5 private-memory allocation encodings. #![allow(dead_code)] -use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use super::initdata::{Board, J714}; @@ -19,8 +19,27 @@ pub(crate) fn select_render_pool(board: &Board) { let (block, pool) = board.render_pool; RENDER_BLOCK_BYTES.store(block, Ordering::Relaxed); POOL_BYTES.store(pool, Ordering::Relaxed); + J813_POOLS.store(board.j813_forms(), Ordering::Relaxed); } +/// macOS 27.0 on the J813 (common 26A428) keeps its pools in the G17P form: a 0x68-byte +/// control (run cursor +0x48, state +0x4c, completed +0x54, class +0x60), a run list of +/// 0x40-byte slots, and four-word page-pool rows on a 0x20 stride. +static J813_POOLS: AtomicBool = AtomicBool::new(false); + +pub(crate) fn j813_pools() -> bool { + J813_POOLS.load(Ordering::Relaxed) +} + +/// Offset of the firmware-completed counter in this board's pool control. +pub(crate) fn completed_offset() -> usize { + if j813_pools() { J813_COMPLETED } else { COMPLETED } +} + +const J813_COMPLETED: usize = 0x54; +/// Words of one J813 page-pool row: the first four words of the M5 descriptor. +pub(crate) const J813_ROW_WORDS: usize = 4; + pub(crate) fn render_block_bytes() -> usize { RENDER_BLOCK_BYTES.load(Ordering::Relaxed) } @@ -68,7 +87,8 @@ impl PoolState { words[1] >> 32 & 0x0fff_ffff, words[2] & 0x0fff_ffff, words[5] & 0x0fff_ffff, words[6] & 0xffff, words[7] & 0x0fff_ffff, words[7] >> 32 & 0xffff]; - for (offset, value) in SAVED_FIELDS.into_iter().zip(values) { + let fields = if j813_pools() { 4 } else { SAVED_FIELDS.len() }; + for (offset, value) in SAVED_FIELDS.into_iter().zip(values).take(fields) { self.0[offset..offset + 4].copy_from_slice(&(value as u32).to_le_bytes()); } } @@ -135,7 +155,7 @@ impl PoolGeometry { } pub(crate) fn g17s_render(blocks: usize, directory_entries: u32) -> Result { - Self::with_kind(blocks, directory_entries, true) + Self::with_kind(blocks, directory_entries, !j813_pools()) } pub(crate) fn block_bytes(self) -> usize { @@ -264,9 +284,32 @@ pub(crate) fn encode_state(p: PoolLayout, submitted: u64, out: &mut [u8]) Ok(()) } +/// The J813's G17P-form pool control, as macOS 27.0 leaves it on that machine. +fn encode_j813_state(p: PoolLayout, submitted: u64, out: &mut [u8], compute: bool) + -> Result<(), PoolError> +{ + validate_layout(p)?; + if submitted == 0 || submitted & 3 != 0 { return Err(PoolError::Address); } + if out.len() < STATE_BYTES { return Err(PoolError::Buffer); } + out[..STATE_BYTES].fill(0); + // Run-list qwords in use: one 0x40-byte slot per block. + let run_qwords = if p.geometry.g17s_render_blocks { 1 } else { 8 }; + for (offset, value) in [(0, if compute { 3 } else { 1 }), (8, p.hardware_id), (0x10, 1), + (0x1c, p.geometry.directory_entries), (0x24, p.geometry.pages), + (0x2c, p.geometry.pages), (0x48, (p.geometry.blocks * run_qwords) as u32), + (0x5c, u32::from(compute)), (0x60, if compute { 1 } else { 2 })] { + out[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + for (offset, value) in [(0x14, p.directory), (0x30, p.freelist), (0x4c, submitted)] { + out[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + Ok(()) +} + pub(crate) fn encode_g17s_render_state(p: PoolLayout, submitted: u64, out: &mut [u8]) -> Result<(), PoolError> { + if j813_pools() { return encode_j813_state(p, submitted, out, false); } encode_state(p, submitted, out)?; out[0x80..0x84].copy_from_slice(&4u32.to_le_bytes()); Ok(()) @@ -275,6 +318,7 @@ pub(crate) fn encode_g17s_render_state(p: PoolLayout, submitted: u64, out: &mut pub(crate) fn encode_g17s_compute_state(p: PoolLayout, submitted: u64, out: &mut [u8]) -> Result<(), PoolError> { + if j813_pools() { return encode_j813_state(p, submitted, out, true); } if !p.geometry.g17s_render_blocks { return Err(PoolError::Geometry); } encode_state(p, submitted, out)?; out[0x7c..0x80].copy_from_slice(&1u32.to_le_bytes()); diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index a61488a5ddce1..4e0317de32ac1 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -420,6 +420,17 @@ impl InitData { /// One M5 page-pool row: the pool's eight descriptor words at its 64-byte slot. fn set_m5_uma(&self, slot: u16, words: [u64; 8]) -> Result { let object = self.object(self.page_pool)?; + if super::fw::m5::pool::j813_pools() { + // The J813's rows are the descriptor's first four words, 0x20 apart. + const ROW: usize = super::fw::m5::pool::J813_ROW_WORDS * 8; + let offset = usize::from(slot) * ROW; + object.pointer(offset, ROW)?; + for (index, word) in words.into_iter().take(ROW / 8).enumerate() { + object.dword(offset + index * 8)?.store(word, Ordering::Relaxed); + } + fence(Ordering::SeqCst); + return Ok(()); + } let offset = usize::from(slot) * core::mem::size_of::<[u64; 8]>(); // Check the full row before the first store. object.pointer(offset, core::mem::size_of::<[u64; 8]>())?; diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs index ab94764580521..a070df1d20d53 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs @@ -122,6 +122,17 @@ impl Pools { } fn stage(&self, init: &InitData, pool: &RenderPool) -> Result { + if pool.is_m5() && crate::g17::fw::m5::pool::j813_pools() { + // The J813's rows are the descriptor's first four words, 0x20 apart. + let words = descriptor_words::<4>(init.page_pool_descriptor_table()?, pool.id())?; + pool.repopulate()?; + let descriptor = pool.m5_descriptor()?; + for (word, value) in words.into_iter().zip(descriptor) { + word.store(value, Ordering::Relaxed); + } + fence(Ordering::SeqCst); + return Ok(()); + } if pool.is_m5() { let words = descriptor_words::<8>(init.page_pool_descriptor_table()?, pool.id())?; pool.repopulate()?; @@ -250,6 +261,16 @@ impl Pools { /// A detached pool may capture firmware counters only after its last pair /// stopped using this registration. Callers check all retained pair owners. pub(super) fn save(&self, init: &InitData, pool: &RenderPool) -> Result { + if pool.is_m5() && crate::g17::fw::m5::pool::j813_pools() { + let words = descriptor_words::<4>(init.page_pool_descriptor_table()?, pool.id())?; + fence(Ordering::Acquire); + // The row carries only the first four words; the rest are the pool's own. + let mut descriptor = pool.m5_descriptor()?; + for (value, word) in descriptor.iter_mut().zip(words) { + *value = word.load(Ordering::Relaxed); + } + return pool.store_m5_descriptor(descriptor); + } if pool.is_m5() { let words = descriptor_words::<8>(init.page_pool_descriptor_table()?, pool.id())?; fence(Ordering::Acquire); From ab6729b43ddaa87c525888d87c2d4b04c8f80c29 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:55 +0100 Subject: [PATCH 28/30] drm/asahi: g17: M5: map the J813's render metrics in context 0 With parallel clients the J813's firmware took fragment MMU faults reading 0x10_0000_0040 in VM slot 0. It touches a render's metrics in context 0, whose user root is the kernel lower VM, at the client's compact-window address. Map each client's metrics entry at the compact window's start plus the entry's offset in the shared backing. Map the whole backing at the window's start in the kernel lower VM too, and link that window's table before the firmware samples the lower root's top-level entries. If the fixed address is taken, a client falls back to any free address in the window, as before. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/buffer.rs | 16 ++++++++++++++-- drivers/gpu/drm/asahi/g17/initdata.rs | 14 ++++++++++++++ drivers/gpu/drm/asahi/g17/mod.rs | 8 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer.rs b/drivers/gpu/drm/asahi/g17/buffer.rs index 0747a1fe69c87..8a99ddbd6daca 100644 --- a/drivers/gpu/drm/asahi/g17/buffer.rs +++ b/drivers/gpu/drm/asahi/g17/buffer.rs @@ -581,8 +581,20 @@ impl MetricsLease { } let pitch = self.allocator.entry_pitch; let offset = self.slot.checked_mul(pitch).ok_or(EOVERFLOW)?; - let client = self.backing.map_range(vm, offset..offset + pitch, BlockGeometry::M5_COMPACT, - mmu::UAT_PGSZ as u64, mmu::PROT_GPU_SHARED_RW, 0)?; + // The J813's firmware touches a render's metrics in context 0, whose user root is the + // kernel lower VM, at the client's address. That VM maps the whole backing at the compact + // window's start (InitData::bind_m5_metrics), so place the entry where its offset puts + // it there. + let fixed = super::J813_FORMS.load(Ordering::Relaxed).then(|| { + let at = BlockGeometry::M5_COMPACT.start + offset as u64; + self.backing.map_range(vm, offset..offset + pitch, at..at + pitch as u64, + mmu::UAT_PGSZ as u64, mmu::PROT_GPU_SHARED_RW, 0).ok() + }).flatten(); + let client = match fixed { + Some(client) => client, + None => self.backing.map_range(vm, offset..offset + pitch, BlockGeometry::M5_COMPACT, + mmu::UAT_PGSZ as u64, mmu::PROT_GPU_SHARED_RW, 0)?, + }; self.addresses.client = client.iova(); self.client = Some(client); Ok(()) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 4e0317de32ac1..eefcb948ffda0 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -342,6 +342,20 @@ impl InitData { mmu::UAT_PGSZ as u64, mmu::PROT_FW_SHARED_RW)?; self.metrics_fw_va = alias.iova(); self.aliases.push(alias, GFP_KERNEL)?; + if super::J813_FORMS.load(Ordering::Relaxed) { + // The J813's firmware touches a render's metrics in context 0 (the kernel lower VM) + // at the client's compact-window address. Every client VM maps its metrics entry at + // the window's start plus the entry's offset (MetricsLease::map_client), so map the + // whole backing at the window's start here. + let start = super::buffer::BlockGeometry::M5_COMPACT.start; + let alias = metrics.alias_in(uat.kernel_lower_vm(), + start..start + metrics.size() as u64, mmu::UAT_PGSZ as u64, + mmu::PROT_GPU_SHARED_RW)?; + if alias.iova() != start { + return Err(EINVAL); + } + self.aliases.push(alias, GFP_KERNEL)?; + } Ok(()) } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 42745e4a8cba7..d3ddb72342b2a 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1870,6 +1870,14 @@ impl Gpu { board.graph, )) .inspect_err(at("lower links"))?; + // The J813's firmware runs part of a render in context 0, whose user root is the kernel + // lower VM, and touches the render's metrics through their compact-window address + // there (InitData::bind_m5_metrics): link that window's table too. + if J813_FORMS.load(core::sync::atomic::Ordering::Relaxed) { + let compact = buffer::BlockGeometry::M5_COMPACT.start; + uat.prepare_lower_tables(&[compact..compact + mmu::UAT_PGSZ as u64]) + .inspect_err(at("lower links"))?; + } // The J813 in native order, from aura's trace of macOS 27.0 on that machine: the start // control is written first (macOS does it at platform init), the coprocessors are started // next, and the Fender gating is cleared only once both of them run -- not before them. From 3e0b58b27941131f310802d9fe1f43604288ade4 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 22:17:55 +0100 Subject: [PATCH 29/30] drm/asahi: g17: grow a parameter buffer only as far as it holds The host's pre-render growth target is an upper bound from the tile count, and the buffer's capacity was fixed when it was created for the pair's first render. A 16384x16384 render without attachments after smaller ones asked for more blocks than the buffer can hold. The render then failed with ERANGE and the client lost the device (dEQP-GLES31.functional.fbo.no_attachments.maximums.*). Clamp the target to the buffer's capacity, as firmware_target() does for the firmware's own requests. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/buffer.rs | 4 ++++ drivers/gpu/drm/asahi/g17/queue/render.rs | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer.rs b/drivers/gpu/drm/asahi/g17/buffer.rs index 8a99ddbd6daca..7a6e1f9837942 100644 --- a/drivers/gpu/drm/asahi/g17/buffer.rs +++ b/drivers/gpu/drm/asahi/g17/buffer.rs @@ -845,6 +845,10 @@ impl Manager { pub(crate) fn firmware_target(&self) -> usize { self.blocks().saturating_add(fw::GROW_BLOCKS).min(self.tvb.geometry.max_blocks()) } + /// The most blocks this buffer's geometry can hold. + pub(crate) fn max_blocks(&self) -> usize { + self.tvb.geometry.max_blocks() + } /// Read-only diagnostic while firmware is halted. In particular this /// does not reset the high-water counter as automatic_target() does. pub(crate) fn report_recovery(&self) { diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 03dacf1734f51..33fcba8732779 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1660,7 +1660,12 @@ impl Pair { } else { 0 }; - let required = required.max(feedback); + // The estimate is an upper bound from the tile count, and the buffer's capacity was + // fixed when it was created for this pair's first render. Grow as far as it goes, as the + // firmware's own requests do (firmware_target), instead of failing the render with + // ERANGE: a 16384x16384 no-attachment render after smaller ones lost the device + // (dEQP-GLES31.functional.fbo.no_attachments.maximums.*). + let required = required.max(feedback).min(self.memory.manager.max_blocks()); Ok((required > self.memory.manager.blocks()).then_some(required)) } From 7595d7508fb7b0ad5a8e6226bf6bc600f1f3159c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 9 Nov 2026 23:46:16 +0100 Subject: [PATCH 30/30] drm/asahi: g17: M5: on the J813, a tiling kick waits for the last fragment kick With several clients, the J813 took a few GPU recoveries per conformance run (GLES31 at four jobs: 5 to 9). Every one was the same MMU fault: the ISP parameter fetch (requestor 0x9a) reading a render's own tile data in its compact window, at level 1, while the hardware had another context's VM slot bound, or slot 0. The firmware then blamed the render's 3D queue. One client at a time never faulted. macOS 27.0 never lets those overlap. In its own kick queues, read from four captures of the J813 under aura (256 TA kicks each), 239 TA kicks name a 3D kick as a dependency, and every one of them names the 3D kick of the immediately preceding render. This driver gave a tiling kick no dependency on earlier fragment work at all, so with parallel clients one context's TA ran beside another's 3D, and that 3D's parameter fetch went through the TA's VM. On the J813, a tiling kick now also waits, in class 2, for the last fragment kick published on any pair, which is the order macOS keeps. These did not change the faults: - sending the TA configuration without its parameter-buffer binding, as macOS 27.0 does in steady state; - growing every parameter buffer to capacity, so no render needs a partial render; - listing the live contexts' scheduler records at +0x800 of the PB region, as macOS has them. At four jobs, with this ordering: - GLES2: 16955 pass, 0 fail, 0 recoveries; - GLES3: 44379 pass, 1 fail, 0 recoveries; - GLES31: 37337 pass, 2 fail, 1 flake, 0 recoveries (it ran in 3m00s; 4m03s before, with the recoveries). The remaining failures fail without it too. The GLES31 ones are the image_load_store qualifier cases, which also fail under parallel load on the 26.x kernel. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/queue/render.rs | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 33fcba8732779..e4a9e662dc963 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -614,6 +614,10 @@ struct RenderBarriers { fragment_types: [u8; 5 + crate::g17::hazard::PUBLICATION_BARRIERS], } +/// The last fragment kick published on any pair: on the J813 the next tiling kick waits for it +/// (see [`Pair::publish`]). +static J813_LAST_FRAGMENT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + fn render_barriers( fragment_stage: bool, stage_prefixes: bool, @@ -907,9 +911,28 @@ impl Pair { return Err(EINVAL); }; let own_tiling = KickDependency::new(qids[0], kicks[0]).ok_or(EINVAL)?; - let barriers = + let mut barriers = render_barriers(pass.fragment_barriers, pass.stage_prefixes, dependencies, own_tiling, self.board.map_or(2, crate::g17::fw::m5::compute::explicit_dependency)); + // On the J813 a tiling kick also waits for the last fragment kick published, on any + // pair, as macOS 27.0 orders them there: its TA kick for render n names render n - 1's + // 3D kick (every such dependency in its captured queues). Without it a TA ran beside + // another context's 3D, and that 3D fetched its own tile data (ISP parameter fetch, + // requestor 0x9a) through the TA's VM slot, or slot 0, and faulted. + if j813 { + let last = KickDependency::from_word( + J813_LAST_FRAGMENT.load(core::sync::atomic::Ordering::Acquire), + ) + .filter(|d| d.word() != 0 && d.qid() != qids[0]); + if let Some(last) = last { + if barriers.tiling_count < barriers.tiling.len() { + barriers.tiling[barriers.tiling_count] = last; + barriers.tiling_types[barriers.tiling_count] = 2; + barriers.tiling_count += 1; + barriers.tiling_types[barriers.tiling_count] = 1; + } + } + } let tiling_barriers = &barriers.tiling[..barriers.tiling_count]; // With the G17D forms a fragment kick with history also names its parent's // predecessor, in class 1, after the self-parent (classes [1, 3, 1] for a lone pass). @@ -1181,6 +1204,7 @@ impl Pair { self.ordinal = next_ordinal; if let Some(fragment) = KickDependency::new(qids[1], kicks[1]) { packet.completion.note_render_publication(fragment); + J813_LAST_FRAGMENT.store(fragment.word(), core::sync::atomic::Ordering::Release); } self.cursors = windows.map(|window| window.target()); for stage in 0..2 {