From c89018ed79c338c40eb67080b3005235060fedd6 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:18:41 -0400 Subject: [PATCH 01/15] drm/apple: accept automatic PMGR activity in J613 coldplug Use the PMGR provider active-state predicate when checking inherited display power. DISP_CPU can report ACTUAL=4 with AUTO_ENABLE and TARGET=15 after automatic clock gating. Keep the checks read-only and preserve all other handoff requirements. Test all actual, target and automatic-state combinations and the captured J613 state through the production helper. Signed-off-by: Satya Benson --- drivers/gpu/drm/apple/j613_dcp_activate.c | 9 ++++- tools/asahi/test-j613-dcp-power.py | 45 +++++++++++++++++++++++ tools/aurora-ci/host-tests.json | 8 ++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tools/asahi/test-j613-dcp-power.py diff --git a/drivers/gpu/drm/apple/j613_dcp_activate.c b/drivers/gpu/drm/apple/j613_dcp_activate.c index 6e20a5afc914c5..6d0a2620694692 100644 --- a/drivers/gpu/drm/apple/j613_dcp_activate.c +++ b/drivers/gpu/drm/apple/j613_dcp_activate.c @@ -40,6 +40,13 @@ bool apple_t8122_25g83_board(void) return ok; } +/* Match the PMGR provider: automatic domains can be clock-gated while active. */ +static bool j613_power_state_active(u32 state) +{ + return FIELD_GET(GENMASK(7, 4), state) == 15 || + ((state & BIT(28)) && FIELD_GET(GENMASK(3, 0), state) == 15); +} + /* These are DT domain nodes, carrying the PMGR offsets, not copied registers. */ static int j613_power_ready(void) { @@ -82,7 +89,7 @@ static int j613_power_ready(void) return -ENOMEM; state = readl(pmgr + offset); iounmap(pmgr); - if (FIELD_GET(GENMASK(7, 4), state) != 15) + if (!j613_power_state_active(state)) return -EBUSY; } return 0; diff --git a/tools/asahi/test-j613-dcp-power.py b/tools/asahi/test-j613-dcp-power.py new file mode 100644 index 00000000000000..4e93bcc8b1bdb1 --- /dev/null +++ b/tools/asahi/test-j613-dcp-power.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the production J613 PMGR predicate and test all actual/target/auto states.""" +from pathlib import Path +import os +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/j613_dcp_activate.c").read_text() +start = source.index("static bool j613_power_state_active(") +end = source.index("\n}\n", start) + 3 +stub = r""" +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U << (n)) +#define GENMASK(a,b) (((1U << ((a)-(b)+1))-1) << (b)) +#define FIELD_GET(mask,v) (((v) & (mask)) >> __builtin_ctz(mask)) +""" +main = r""" +int main(void) { + for (u32 actual = 0; actual < 16; actual++) + for (u32 target = 0; target < 16; target++) + for (u32 automatic = 0; automatic < 2; automatic++) { + u32 word = (actual << 4) | target | (automatic << 28); + bool expected = actual == 15 || (target == 15 && automatic); + if (j613_power_state_active(word) != expected) return 1; + } + if (!j613_power_state_active(0x1404024f)) return 2; + if (j613_power_state_active(0x0404024f)) return 3; + if (j613_power_state_active(0x14040240)) return 4; + puts("PASS: 512 PMGR combinations and captured-state regression controls"); + return 0; +} +""" +with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "power.c" + path.write_text(stub + source[start:end] + main) + for flags in ([], ["-O2"]): + exe = str(path.with_suffix("")) + subprocess.run([os.environ.get("CC", "cc"), "-Wall", "-Wextra", "-Werror", + *flags, str(path), "-o", exe], check=True) + subprocess.run([exe], check=True) diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index a1b93d695dfd6e..6324bcaa99a94a 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2282,6 +2282,14 @@ "{output}" ], "note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations." + }, + { + "name": "tools/asahi/test-j613-dcp-power.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-j613-dcp-power.py" + ] } ], "helpers": { From d2c35b616d4a44f1e37843b494055657bdc62e54 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:18:41 -0400 Subject: [PATCH 02/15] drm/asahi: accept the T8122 25G83 mailbox compatible list The 25G83 device tree advertises the AGX-specific compatible followed by asc-mailbox-v4. Both bind the same mailbox operations. Admit this exact pair while retaining all resource and identity checks. Exercise the production rejection predicate with public-DTS fixtures and resource mutations; allow optional local DT captures for reproduction. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/m3/m3_soc.rs | 1 + tools/asahi/test-m3-mailbox.py | 90 ++++++++++++++++++++++++++++++ tools/aurora-ci/host-tests.json | 8 +++ 3 files changed, 99 insertions(+) create mode 100644 tools/asahi/test-m3-mailbox.py diff --git a/drivers/gpu/drm/asahi/m3/m3_soc.rs b/drivers/gpu/drm/asahi/m3/m3_soc.rs index 4b81475612b6fa..7500822c4124c5 100644 --- a/drivers/gpu/drm/asahi/m3/m3_soc.rs +++ b/drivers/gpu/drm/asahi/m3/m3_soc.rs @@ -291,6 +291,7 @@ pub(crate) static T8122: Soc = Soc { cores_per_cluster: 10, mailbox_compatibles: &[ b"apple,t8122-agx-asc-mailbox\0", + b"apple,t8122-agx-asc-mailbox\0apple,asc-mailbox-v4\0", b"apple,t8122-asc-mailbox\0apple,asc-mailbox-v4\0", ], mailbox_interrupts: &[0, 723, 4, 0, 724, 4, 0, 725, 4, 0, 726, 4], diff --git a/tools/asahi/test-m3-mailbox.py b/tools/asahi/test-m3-mailbox.py new file mode 100644 index 00000000000000..2320d17f10f6f4 --- /dev/null +++ b/tools/asahi/test-m3-mailbox.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the production T8122 mailbox predicate with captured DTB properties. + +Usage: python3 tools/asahi/test-m3-mailbox.py [runtime.dtb daily.dtb] +Requires dtc's fdtget. No target access or MMIO. +""" +import hashlib +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +board = (ROOT / 'drivers/gpu/drm/asahi/m3/m3_board.rs').read_text() +soc = (ROOT / 'drivers/gpu/drm/asahi/m3/m3_soc.rs').read_text() +t8122 = soc[soc.index('pub(crate) static T8122:'):] +compatibles = re.search(r'mailbox_compatibles: &(\[.*?\]),', t8122, re.S)[1] +irqs = re.search(r'mailbox_interrupts: &(\[.*?\]),', t8122, re.S)[1] +base = re.search(r'mailbox: (0x[0-9a-fA-F_]+),', t8122)[1] +regfn = re.search(r'fn expected_mailbox_reg\(.*?\n}', board, re.S)[0] +predicate = board[board.index(' if !soc.mailbox_compatibles'):] +predicate = predicate[:predicate.index('\n {')].strip()[3:] + + +def prop(dtb, node, name, kind): + result = subprocess.check_output(['fdtget', '-t', kind, str(dtb), node, name], text=True) + return [int(v, 16) for v in result.split()] + + +def vec(values): + return 'vec![' + ','.join(map(str, values)) + ']' + + +fixtures = [] +for argument in sys.argv[1:]: + dtb = Path(argument) + gpu = '/soc/gpu@292400000' + # Both retained captures use this mailbox path; verify the phandle link, + # not its capture-specific numeric value. + mbox = '/soc/mbox@292408000' + if prop(dtb, gpu, 'mboxes', 'x') != prop(dtb, mbox, 'phandle', 'x'): + raise RuntimeError('GPU mailbox phandle mismatch') + values = [prop(dtb, mbox, name, kind) for name, kind in + [('compatible', 'bx'), ('reg', 'x'), ('interrupt-names', 'bx'), + ('interrupts', 'x'), ('#mbox-cells', 'x')]] + if len(values[4]) != 1: + raise RuntimeError('bad #mbox-cells length') + fixtures.append('check(' + ','.join(vec(v) for v in values[:4]) + ',' + str(values[4][0]) + ');') + print(dtb.name, hashlib.sha256(dtb.read_bytes()).hexdigest()) +if not sys.argv[1:]: + # Public t8122-j613-dcp-25g83.dtsi and t8122-gpu.dtsi resource contract. + names = list(b'send-empty\0send-not-empty\0recv-empty\0recv-not-empty\0') + for compat in (b'apple,t8122-agx-asc-mailbox\0apple,asc-mailbox-v4\0', + b'apple,t8122-asc-mailbox\0apple,asc-mailbox-v4\0'): + values = [list(compat), [2, 0x92408000, 0, 0x4000], names, + [0,723,4,0,724,4,0,725,4,0,726,4]] + fixtures.append('check(' + ','.join(vec(v) for v in values) + ',0);') +if len(fixtures) != 2: + raise SystemExit('supply runtime.dtb and daily.dtb') + +source = ''' +struct Windows { mailbox: u64 } +struct Soc { mailbox_compatibles: &'static [&'static [u8]], mailbox_interrupts: &'static [u32], windows: Windows } +''' + regfn + ''' +fn accepts(soc: &Soc, compat: &Vec, mbox_reg: &Vec, irq_names: &Vec, irqs: &Vec, cells: u32) -> bool { + !(''' + predicate + ''') +} +fn check(compat: Vec, reg: Vec, names: Vec, irqs: Vec, cells: u32) { + let soc = Soc { mailbox_compatibles: &''' + compatibles + ''', mailbox_interrupts: &''' + irqs + ''', windows: Windows { mailbox: ''' + base + ''' } }; + let ok = |c: &Vec, r: &Vec, n: &Vec, i: &Vec, x| accepts(&soc,c,r,n,i,x); + assert!(ok(&compat,®,&names,&irqs,cells)); + for i in 0..reg.len() { let mut v=reg.clone(); v[i]^=1; assert!(!ok(&compat,&v,&names,&irqs,cells)); } + for i in 0..irqs.len() { let mut v=irqs.clone(); v[i]^=1; assert!(!ok(&compat,®,&names,&v,cells)); } + assert!(!ok(&compat,®,&names,&irqs,1)); + let mut v=names.clone(); v[0]^=1; assert!(!ok(&compat,®,&v,&irqs,cells)); + for v in [b"apple,asc-mailbox-v4\\0".to_vec(), b"apple,t8122-agx-asc-mailbox\\0unknown\\0".to_vec(), b"apple,t6031-agx-asc-mailbox\\0".to_vec()] { assert!(!ok(&v,®,&names,&irqs,cells)); } + for v in soc.mailbox_compatibles { assert!(ok(&v.to_vec(),®,&names,&irqs,cells)); } +} +fn main() { +''' + '\n'.join(fixtures) + '\nprintln!("mailbox: both captures, all exact supported lists and resource/identity mutation controls PASS");\n}\n' +with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) + (path / 'test.rs').write_text(source) + subprocess.run([os.environ.get('RUSTC', 'rustc'), '--edition=2021', str(path / 'test.rs'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) +print('m3_board.rs', hashlib.sha256(board.encode()).hexdigest()) +print('m3_soc.rs', hashlib.sha256(soc.encode()).hexdigest()) diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index a1b93d695dfd6e..017828e7865d44 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2282,6 +2282,14 @@ "{output}" ], "note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations." + }, + { + "name": "tools/asahi/test-m3-mailbox.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-mailbox.py" + ] } ], "helpers": { From d17068fc1f78fcde813d4169c05c1db37aa61fcf Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 22:17:26 -0400 Subject: [PATCH 03/15] test: vary unrelated bits in J613 PMGR state words Cross all 512 ACTUAL/TARGET/AUTO combinations with zero, all-set, alternating and individual unrelated-bit patterns. This verifies that readiness ignores every bit outside the three PMGR fields. A temporary ACTUAL mask widened from GENMASK(7, 4) to GENMASK(8, 4) passes the old cases but fails the new ones at both -O0 and -O2. Make the unoptimized compiler flag explicit and report failing words. Signed-off-by: Satya Benson --- tools/asahi/test-j613-dcp-power.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tools/asahi/test-j613-dcp-power.py b/tools/asahi/test-j613-dcp-power.py index 4e93bcc8b1bdb1..3bc1a3379c253e 100644 --- a/tools/asahi/test-j613-dcp-power.py +++ b/tools/asahi/test-j613-dcp-power.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Compile the production J613 PMGR predicate and test all actual/target/auto states.""" +"""Compile the production J613 PMGR predicate and test PMGR fields and unrelated bits.""" from pathlib import Path import os import subprocess @@ -21,24 +21,36 @@ """ main = r""" int main(void) { + /* Only ACTUAL[7:4], TARGET[3:0] and AUTO_ENABLE[28] affect readiness. */ + const u32 unrelated_mask = ~(GENMASK(7, 0) | BIT(28)); + u32 unrelated[32] = {0, unrelated_mask, + 0xaaaaaaaaU & unrelated_mask, + 0x55555555U & unrelated_mask}; + unsigned int count = 4; + for (unsigned int bit = 8; bit < 32; bit++) + if (bit != 28) unrelated[count++] = BIT(bit); + for (unsigned int i = 0; i < count; i++) for (u32 actual = 0; actual < 16; actual++) for (u32 target = 0; target < 16; target++) for (u32 automatic = 0; automatic < 2; automatic++) { - u32 word = (actual << 4) | target | (automatic << 28); + u32 word = unrelated[i] | (actual << 4) | target | (automatic << 28); bool expected = actual == 15 || (target == 15 && automatic); - if (j613_power_state_active(word) != expected) return 1; + if (j613_power_state_active(word) != expected) { + fprintf(stderr, "PMGR state 0x%08x: expected active=%d\n", word, expected); + return 1; + } } if (!j613_power_state_active(0x1404024f)) return 2; if (j613_power_state_active(0x0404024f)) return 3; if (j613_power_state_active(0x14040240)) return 4; - puts("PASS: 512 PMGR combinations and captured-state regression controls"); + puts("PASS: 13824 PMGR combinations and captured-state regression controls"); return 0; } """ with tempfile.TemporaryDirectory() as temporary: path = Path(temporary) / "power.c" path.write_text(stub + source[start:end] + main) - for flags in ([], ["-O2"]): + for flags in (["-O0"], ["-O2"]): exe = str(path.with_suffix("")) subprocess.run([os.environ.get("CC", "cc"), "-Wall", "-Wextra", "-Werror", *flags, str(path), "-o", exe], check=True) From 2bf9dbd571c8b357099f243bb730350e7c917b7a Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:38:58 -0400 Subject: [PATCH 04/15] drm/asahi: describe the T8122 extended firmware profile and owned IO records Separate firmware identity, HAL metadata and VM bounds from physical G15G stream selection. Add checked root and owned IO descriptor encoders. Introduce slot 28 at SGX+0xd50000 with a production-writer regression; the shifted T8132 bank is never introduced. Keep VM padding zero. Signed-off-by: Satya Benson --- MAINTAINERS | 2 + drivers/gpu/drm/asahi/m3/m3_client.rs | 17 +- drivers/gpu/drm/asahi/m3/m3_drm.rs | 20 +- drivers/gpu/drm/asahi/m3/m3_firmware.rs | 146 +++- drivers/gpu/drm/asahi/m3/m3_init_26.rs | 275 +++++++ drivers/gpu/drm/asahi/m3/m3_init_layout.rs | 139 +++- drivers/gpu/drm/asahi/m3/m3_mmio_26.rs | 265 +++++++ drivers/gpu/drm/asahi/m3/m3_profile.rs | 69 ++ drivers/gpu/drm/asahi/m3/mod.rs | 4 + drivers/gpu/drm/asahi/m3/t8122_admission.rs | 49 +- tools/asahi/m3-firmware-tests.rs | 14 + tools/asahi/m3-runtime-tests.rs | 9 + tools/asahi/m3-uapi-fixture.c | 363 +++++++++ tools/asahi/m3-uapi-tests.rs | 832 ++++++++++++++++++++ tools/asahi/test-m3-mailbox.py | 11 + tools/asahi/test-m3-uapi.py | 73 ++ tools/aurora-ci/host-tests.json | 31 +- 17 files changed, 2295 insertions(+), 24 deletions(-) create mode 100644 drivers/gpu/drm/asahi/m3/m3_init_26.rs create mode 100644 drivers/gpu/drm/asahi/m3/m3_mmio_26.rs create mode 100644 drivers/gpu/drm/asahi/m3/m3_profile.rs create mode 100644 tools/asahi/m3-firmware-tests.rs create mode 100644 tools/asahi/m3-uapi-fixture.c create mode 100644 tools/asahi/m3-uapi-tests.rs create mode 100755 tools/asahi/test-m3-uapi.py diff --git a/MAINTAINERS b/MAINTAINERS index 491823243a5fc1..d152848f6be827 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2723,7 +2723,9 @@ F: drivers/pwm/pwm-apple.c F: drivers/reset/reset-apple-cio.c F: drivers/rtc/rtc-macsmc.c F: drivers/soc/apple/* +F: tools/asahi/m3-uapi-* F: tools/asahi/pmp-bootargs-tests.rs +F: tools/asahi/test-m3-uapi.py F: drivers/spi/spi-apple.c F: drivers/spmi/spmi-apple-controller.c F: drivers/thunderbolt/.kunitconfig.apple-dpin diff --git a/drivers/gpu/drm/asahi/m3/m3_client.rs b/drivers/gpu/drm/asahi/m3/m3_client.rs index dff1af0783965d..c1a07d61120d13 100644 --- a/drivers/gpu/drm/asahi/m3/m3_client.rs +++ b/drivers/gpu/drm/asahi/m3/m3_client.rs @@ -35,7 +35,7 @@ use crate::{ const AGX3_PARAMS_SIZE: u64 = (core::mem::offset_of!(uapi::drm_asahi_params_global, gpu_hal_generation) + core::mem::size_of::()) as u64; -const _: () = assert!(AGX3_PARAMS_SIZE == 600); +const _: () = assert!(AGX3_PARAMS_SIZE == super::m3_profile::PARAMS_REQUIRED); /// Refused clients that are logged per boot; later refusals only count. const REFUSALS_LOGGED: u32 = 16; @@ -45,10 +45,19 @@ static REFUSALS: AtomicU32 = AtomicU32::new(0); /// [`AGX3_PARAMS_SIZE`] bytes (so it knows this GPU runs AGX3 command streams), and pass /// VM_CREATE flags 0 (legacy HAL). `size` is the largest GET_PARAMS size the file asked for. pub(crate) fn admit_vm(device: &kernel::device::Device, size: u64, flags: u32) -> Result { + admit_vm_for(device, super::m3_profile::Profile::Legacy, size, flags) +} + +pub(crate) fn admit_vm_for( + device: &kernel::device::Device, + profile: super::m3_profile::Profile, + size: u64, + flags: u32, +) -> Result { { // The positive acknowledgement of the command ABI (7.1.12 `g16_profile::client_admitted`): // GET_PARAMS coverage, and VM_CREATE flags 0 on the legacy-HAL M3 runtime. - if size >= AGX3_PARAMS_SIZE && flags == 0 { + if profile.vm_acknowledged(size, flags) { return Ok(()); } let refusals = REFUSALS.fetch_add(1, Ordering::Relaxed).saturating_add(1); @@ -64,11 +73,13 @@ pub(crate) fn admit_vm(device: &kernel::device::Device, size: u64, flags: u32) - }; dev_info!( device, - "M3: refusing a VM for {}[{}]: it asked GET_PARAMS for {} of the {} parameter bytes, so it cannot know this GPU runs AGX3 command streams; M3-aware userspace is needed{}\n", + "M3: refusing a VM for {}[{}]: GET_PARAMS bytes={}/{} HAL={} flags={:#x}{}\n", name, task.group_leader().pid(), size, AGX3_PARAMS_SIZE, + profile.hal(), + flags, last ); } diff --git a/drivers/gpu/drm/asahi/m3/m3_drm.rs b/drivers/gpu/drm/asahi/m3/m3_drm.rs index bcb9fd32c7d255..d81ce66ac53aa0 100644 --- a/drivers/gpu/drm/asahi/m3/m3_drm.rs +++ b/drivers/gpu/drm/asahi/m3/m3_drm.rs @@ -86,6 +86,7 @@ pub(crate) struct Backend { core_mask: u64, max_frequency_khz: u32, soc: &'static crate::m3::m3_soc::Soc, + profile: super::m3_profile::Profile, _progress: Pin>>, _activity: Pin>>, _memory: Pin>>, @@ -125,6 +126,7 @@ impl Backend { let firmware = crate::m3::m3_firmware::identify_loaded(pdev, soc, resources, experiment) .inspect_err(refused("the firmware identity"))?; dev_info!(pdev.as_ref(), "M3: firmware identity accepted\n"); + let profile = firmware.profile(); // InitData source and contents, before any GPU register access. let contents = crate::m3::m3_adt_config::Contents::select(pdev, &firmware, soc, experiment) .inspect_err(refused("the InitData contents"))?; @@ -160,7 +162,7 @@ impl Backend { let completion_work = crate::m3::m3_completion::Completion::new(shared.clone())?; // From here on every failure drops the backend, which stops the runtime. let backend = Arc::new(Self { shared, health, ids: gpu::SequenceIDs::default(), - scheduler, core_mask, max_frequency_khz, soc, + scheduler, core_mask, max_frequency_khz, soc, profile, _progress: progress, _activity: activity, _memory: memory, _timing: timing, _completion: completion, _overlap: overlap }, GFP_KERNEL)?; // The completion worker services this runtime from now on; the RTKit callbacks and @@ -208,7 +210,9 @@ impl gpu::Gpu for Backend { } params.max_frequency_khz = self.max_frequency_khz; params.usc_generation = 3; - params.gpu_hal_generation = hw::GpuHalGeneration::Legacy as u32; + params.gpu_hal_generation = self.profile.hal(); + params.max_commands_per_submission = super::agx_uapi::UAPI_MAX_HARDWARE_COMMANDS; + params.max_attachments = super::agx_uapi::UAPI_MAX_ATTACHMENTS as u32; Ok(()) } fn supports_vm_status(&self) -> bool { true } @@ -217,10 +221,12 @@ impl gpu::Gpu for Backend { fn feature_flags(&self) -> u64 { let mut flags = uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_VM_STATUS as u64 | uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_SCHEDULED_QUEUES as u64; - if self.soc.features.fragment_dependency && crate::m3::m3_params::early_tiling() { + if self.profile == super::m3_profile::Profile::Legacy + && self.soc.features.fragment_dependency && crate::m3::m3_params::early_tiling() { flags |= uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_FRAGMENT_DEPENDENCY as u64; } - if self.soc.features.compute_wide_visibility { + if self.profile == super::m3_profile::Profile::Legacy + && self.soc.features.compute_wide_visibility { flags |= uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_COMPUTE_WIDE_VISIBILITY as u64; } flags @@ -233,17 +239,17 @@ impl gpu::Gpu for Backend { } fn uat_geometry(&self) -> mmu::UatGeometry { mmu::UatGeometry { ias: 42, oas: 42 } } fn user_range(&self) -> Range { - mmu::UAT_PGSZ as u64..crate::m3::initdata::RUNTIME_UNKNOWN_PAGE + mmu::UAT_PGSZ as u64..self.profile.user_end() } /// The dummy page sits where InitData tells the firmware it is (unk_30), as on 7.1.12; the /// 42-bit geometry's own unk_page() is a different address. fn unknown_page(&self) -> u64 { - crate::m3::initdata::RUNTIME_UNKNOWN_PAGE + self.profile.user_end() } fn tracked_submission(&self) -> bool { true } /// Only M3-aware userspace may create a VM (7.1.12's per-file client gate). fn admit_vm(&self, dev: &kernel::device::Device, params_size: u64, flags: u32) -> Result { - crate::m3::m3_client::admit_vm(dev, params_size, flags) + crate::m3::m3_client::admit_vm_for(dev, self.profile, params_size, flags) } fn new_vm(&self, range: Range) -> Result { Option::as_mut(&mut *self.shared.lock()).ok_or(ENODEV)?.new_vm(self.ids.vm.next(), range) diff --git a/drivers/gpu/drm/asahi/m3/m3_firmware.rs b/drivers/gpu/drm/asahi/m3/m3_firmware.rs index 0d2835174c6775..c9aa45cc028648 100644 --- a/drivers/gpu/drm/asahi/m3/m3_firmware.rs +++ b/drivers/gpu/drm/asahi/m3/m3_firmware.rs @@ -62,6 +62,19 @@ pub(crate) static KNOWN_IMAGES_T8122: [KnownImage; 1] = [KnownImage { initdata_magic: Some(G15_V14_8_3_INITDATA), }]; +/// Image record for the extended format. The SoC's runtime selection does not use it yet. +pub(crate) static T8122_IMAGES_26: [KnownImage; 1] = [KnownImage { + name: "T8122 C0 firmware-compat 26.6.2", + uuid: None, + stkg_sha256: Some([ + 0x3a, 0x6b, 0x8b, 0x5e, 0x83, 0x38, 0x4d, 0x5c, + 0xc1, 0x92, 0x9d, 0x91, 0xb6, 0x36, 0x40, 0xfe, + 0xe8, 0xf5, 0x52, 0xb5, 0x10, 0x8c, 0x4e, 0xa6, + 0x20, 0xbe, 0xdb, 0x7b, 0xbc, 0xa0, 0x31, 0x64, + ]), + initdata_magic: Some(0x0c89_c357_8392_04b8), +}]; + /// The same T8122 image for the start experiment (`t8122_start`, only with /// `asahi.t8122_start=1`), where `asahi.t8122_initdata_version` may replace the version. pub(crate) static KNOWN_IMAGES_T8122_EXPERIMENT: [KnownImage; 1] = [KnownImage { @@ -117,6 +130,10 @@ pub(crate) struct Layout { /// Offset of the 8-byte tag that precedes the 8 per-boot bytes zeroed before hashing. pub(crate) entropy_tag_offset: usize, pub(crate) entropy_tag: [u8; 8], + /// An additional independent boot value, when required by this image. + extra_boot_value: Option<(usize, [u8; 8])>, + /// Expected boot-table location and byte count in the image header. + boot_table: Option<(u32, u32)>, /// The firmware version, for logs. pub(crate) version: &'static str, } @@ -127,6 +144,8 @@ pub(crate) static T6030_LAYOUT: Layout = Layout { vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0005_c000], entropy_tag_offset: 0x58e88, entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: None, + boot_table: None, version: "RTKit-2419.140.12.release", }; @@ -137,23 +156,73 @@ pub(crate) static T8122_LAYOUT: Layout = Layout { vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0006_4000], entropy_tag_offset: 0x5d54a, entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: None, + boot_table: None, version: "RTKit-2419.140.12.release", }; pub(crate) const TEXT_SHA256: [u8; 32] = [ 0x11, 0xe4, 0x9f, 0x75, 0xb, 0x67, 0x1a, 0x2b, 0x13, 0xd0, 0x92, 0xdb, 0x1c, 0xbc, 0xa3, 0xa8, 0x77, 0x55, 0x86, 0xd6, 0xb1, 0xe3, 0xaa, 0xe8, 0xa6, 0xa4, 0x64, 0xa7, 0x80, 0x2, 0x93, 0xd5]; +/// T8122 C0, firmware-compat 26.6.2. Selection remains separate from runtime admission. +pub(crate) static T8122_LAYOUT_26: Layout = Layout { + text_size: 393216, + data_size: 1228800, + vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0006_0000], + entropy_tag_offset: 380492, + entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: Some((380719, *b"ECAP\x08\x00\x00\x00")), + boot_table: Some((380492, 561)), + version: "RTKit-3255.160.4.release", +}; + fn normalize_boot_entropy(layout: &Layout, text: &mut [u8]) -> bool { - let tag = layout.entropy_tag_offset; if text.len() != layout.text_size as usize { return false; } - let Some(end) = tag.checked_add(16) else { return false; }; - let Some(entropy) = text.get_mut(tag..end) else { return false; }; - if entropy[..8] != layout.entropy_tag { - return false; + if let Some((offset, count)) = layout.boot_table { + let Some(header) = text.get(0x22c..0x234) else { + return false; + }; + if header[..4] != offset.to_le_bytes() || header[4..] != count.to_le_bytes() { + return false; + } + let Some(end) = (offset as usize).checked_add(count as usize) else { + return false; + }; + if text.get(offset as usize..end).is_none() { + return false; + } + } + let fields = [ + Some((layout.entropy_tag_offset, layout.entropy_tag)), + layout.extra_boot_value, + ]; + // Validate every field before touching even the first boot value. + for (offset, tag) in fields.into_iter().flatten() { + let Some(end) = offset.checked_add(16) else { + return false; + }; + let Some(field) = text.get(offset..end) else { + return false; + }; + if field[..8] != tag { + return false; + } + if let Some((base, count)) = layout.boot_table { + if offset < base as usize || end > base as usize + count as usize { + return false; + } + } + } + if let Some((other, _)) = layout.extra_boot_value { + if other < layout.entropy_tag_offset + 16 && layout.entropy_tag_offset < other + 16 { + return false; + } + } + for (offset, _) in fields.into_iter().flatten() { + text[offset + 8..offset + 16].fill(0); } - entropy[8..].fill(0); true } @@ -167,6 +236,14 @@ pub(crate) struct Firmware { } impl Firmware { + pub(crate) fn profile(&self) -> super::m3_profile::Profile { + if core::ptr::eq(self.layout, &T8122_LAYOUT_26) { + super::m3_profile::Profile::Extended + } else { + super::m3_profile::Profile::Legacy + } + } + pub(crate) const fn version(&self) -> &'static str { self.layout.version } @@ -393,4 +470,61 @@ mod tests { assert!(!normalize_boot_entropy(&layout, &mut text)); assert_eq!(text, vec![0xa5; 15]); } + + fn image_26() -> Vec { + let mut image = vec![0x5a; 393216]; + image[0x22c..0x230].copy_from_slice(&380492u32.to_le_bytes()); + image[0x230..0x234].copy_from_slice(&561u32.to_le_bytes()); + image[380492..380500].copy_from_slice(b"GKTS\x08\x00\x00\x00"); + image[380719..380727].copy_from_slice(b"ECAP\x08\x00\x00\x00"); + image + } + + #[test] + fn newer_image_masks_only_both_boot_values() { + let mut image = image_26(); + let before = image.clone(); + assert!(normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + for index in 0..image.len() { + let expected = if (380500..380508).contains(&index) || (380727..380735).contains(&index) + { + 0 + } else { + before[index] + }; + assert_eq!(image[index], expected, "byte {index}"); + } + } + + #[test] + fn invalid_second_field_or_table_does_not_mask_first_value() { + for index in [0x22c, 0x230, 380492, 380496, 380719, 380723] { + let mut image = image_26(); + image[index] ^= 1; + let before = image.clone(); + assert!(!normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + assert_eq!(image, before); + } + let mut image = image_26(); + image.pop(); + let before = image.clone(); + assert!(!normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + assert_eq!(image, before); + } + + #[test] + fn extended_record_cannot_replace_legacy_identity() { + let newer = &T8122_IMAGES_26[0]; + let digest = newer.stkg_sha256.unwrap(); + assert!(newer.accepts_runtime(None, &digest)); + assert_eq!(newer.initdata_magic, Some(0x0c89_c357_8392_04b8)); + for index in 0..digest.len() { + let mut wrong = digest; + wrong[index] ^= 1; + assert!(!newer.accepts_runtime(None, &wrong)); + } + assert!(!KNOWN_IMAGES_T8122[0].accepts_runtime(None, &digest)); + assert!(!newer.accepts_runtime(KNOWN_IMAGES_T8122[0].uuid, &[0; 32])); + } + } diff --git a/drivers/gpu/drm/asahi/m3/m3_init_26.rs b/drivers/gpu/drm/asahi/m3/m3_init_26.rs new file mode 100644 index 00000000000000..f08f16200b0700 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_init_26.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Allocation and pointer records for the extended M3 initialization format. +//! Owners supply mapped regions; these builders never allocate or publish DMA. + +use super::m3_init_layout::{Error, Region, Root}; +use super::m3_init_storage::{self as storage, Allocation, Initial}; + +pub(crate) const COUNT: usize = 56; +pub(crate) const DYNAMIC_STATE: usize = 49; +pub(crate) const PERFORMANCE_CONTROL: usize = 50; +pub(crate) const RECOVERY_ACTIVITY: usize = 51; +pub(crate) const SENSOR_VALUE: usize = 52; +pub(crate) const RETENTION_STATE: usize = 53; +pub(crate) const GLOBAL_POOL: usize = 54; +pub(crate) const GLOBAL_PARAMETERS: usize = 55; +pub(crate) const RUNTIME_SIZE: usize = 0x4b8; +pub(crate) const LOG_THREADS: usize = 9; +pub(crate) const RX_BYTES: usize = 0x48; +pub(crate) const LOG_PAYLOAD_BYTES: usize = 0xd8; + +/// Extend the existing M3 owner indices instead of introducing another owner graph. +pub(crate) fn allocation(index: usize) -> Result { + let size = match index { + 0..=23 if index % 2 == 0 => 0x30, + 0..=23 => 0x1800, + 24 | 26 | 30 | 32 => 0x30, + 25 => 0x4000, + 27 | 33 => 0x4800, + 28 => LOG_THREADS * 0x30, + 29 => LOG_THREADS * 256 * RX_BYTES, + 31 => 512 * RX_BYTES, + 34 => 0x40, + 35 => 0x3fc0, + 36 => 0xc8, + 37 => 0x4000, + 38 => RUNTIME_SIZE, + 39 => 0x2658, + 40 => 0x88, + 41 => LOG_THREADS * 256 * LOG_PAYLOAD_BYTES, + 42 => 0xc18, + 43 => 0x1248, + 44 => 0xe10, + 45 => 0xe48, + 46 => 0x670, + 47 => 0xeaf0, + 48 => 0x20, + DYNAMIC_STATE | RETENTION_STATE | GLOBAL_POOL | GLOBAL_PARAMETERS => 0x4000, + PERFORMANCE_CONTROL => 0x6578, + RECOVERY_ACTIVITY => 0x60, + SENSOR_VALUE => 4, + _ => return Err(Error::Bounds), + }; + // Separate page-aligned owners, with room for the largest receive allocation. + // This namespace is disjoint from the existing fixed work and timestamp arenas. + Ok(Allocation { + address: 0xffff_fc21_0000_0000 + (index as u64) * 0x10_0000, + size, + gpu_shared: matches!(index, GLOBAL_POOL | GLOBAL_PARAMETERS), + initial: Initial::Zero, + }) +} + +pub(crate) fn cached(index: usize) -> bool { + matches!(index, 38 | 39 | 42 | 43 | 44 | PERFORMANCE_CONTROL) +} + +pub(crate) struct PointerGraph<'a> { + pub(crate) owners: &'a [Region; COUNT], + pub(crate) scratch: Region, + pub(crate) pool_alias: u64, + pub(crate) parameter_alias: u64, +} + +impl PointerGraph<'_> { + /// Check the complete graph, including padded mapping ownership, before encoding it. + fn addresses(&self) -> Result<[u64; COUNT], Error> { + let mut addresses = [0; COUNT]; + for (index, owner) in self.owners.iter().enumerate() { + let size = allocation(index)?.size; + addresses[index] = owner.at(0, size)?; + if addresses[index] & 0x3fff != 0 + || addresses[index] < 0xffff_fc20_0000_0000 + || addresses[index] >= 0xffff_fc2f_fc00_0000 + { + return Err(Error::Address); + } + let mapped = (size + 0x3fff) & !0x3fff; + let end = addresses[index] + .checked_add(mapped as u64) + .ok_or(Error::Address)?; + if end > 0xffff_fc2f_fc00_0000 { + return Err(Error::Address); + } + for prior in 0..index { + let prior_size = (allocation(prior)?.size + 0x3fff) & !0x3fff; + if addresses[index] < addresses[prior] + prior_size as u64 + && addresses[prior] < addresses[index] + mapped as u64 + { + return Err(Error::Address); + } + } + } + let scratch = self.scratch.at(0, 0x8000)?; + let scratch_end = scratch.checked_add(0x8000).ok_or(Error::Address)?; + if scratch & 0x3fff != 0 || scratch_end > 0xffff_fc2f_fc00_0000 { + return Err(Error::Address); + } + for (index, address) in addresses.iter().enumerate() { + let bytes = (allocation(index)?.size + 0x3fff) & !0x3fff; + if scratch < *address + bytes as u64 && *address < scratch_end { + return Err(Error::Address); + } + } + for alias in [self.pool_alias, self.parameter_alias] { + if alias & 0x3fff != 0 || !(0x70_0000_0000..0x70_0200_0000).contains(&alias) { + return Err(Error::Address); + } + } + if self.pool_alias == self.parameter_alias { + return Err(Error::Address); + } + Ok(addresses) + } + + pub(crate) fn root(&self, output: &mut [u8]) -> Result<(), Error> { + self.addresses()?; + Root { + region_a: self.owners[storage::REGION_A], + runtime: self.owners[storage::RUNTIME_POINTERS], + globals: self.owners[storage::GLOBALS], + new_a: self.owners[storage::RUNTIME_FLAGS], + control: self.owners[storage::CONTROL_REGION], + power: self.owners[storage::GLOBALS_POWER], + } + .encode_26(output, self.owners[DYNAMIC_STATE]) + } + + pub(crate) fn runtime(&self, output: &mut [u8]) -> Result<(), Error> { + let addresses = self.addresses()?; + let scratch = self.scratch.at(0, 0x8000)?; + let bytes = output.get_mut(..RUNTIME_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let pointer = |bytes: &mut [u8], offset: usize, value: u64| { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + }; + pointer(bytes, 0x18, scratch); + pointer(bytes, 0x2d0, self.parameter_alias); + pointer(bytes, 0x2e0, self.pool_alias); + bytes[0x250..0x254].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x2f8..0x2fc].copy_from_slice(&4u32.to_le_bytes()); + for (offset, owner) in [ + (0, storage::HARDWARE_DATA), + (0x10, storage::UNKNOWN_PAIR), + (0x1c0, 26), + (0x1c8, 27), + (0x1d0, 28), + (0x1d8, 29), + (0x1e0, 30), + (0x1e8, 31), + (0x1f0, 32), + (0x1f8, 33), + (0x200, storage::FWLOG_PAYLOAD), + (0x254, storage::UNKNOWN_C0), + (0x25c, storage::UNKNOWN_C1), + (0x264, storage::UNKNOWN_C3), + (0x26c, RECOVERY_ACTIVITY), + (0x2d8, GLOBAL_PARAMETERS), + (0x2e8, GLOBAL_POOL), + (0x469, PERFORMANCE_CONTROL), + ] { + pointer(bytes, offset, addresses[owner]); + } + for channel in 0..13 { + let offset = 0x20 + channel * 0x20; + let state = addresses[channel * 2]; + pointer(bytes, offset, state); + pointer(bytes, offset + 8, state + 0x10); + pointer(bytes, offset + 16, state + 0x20); + pointer(bytes, offset + 24, addresses[channel * 2 + 1]); + } + Ok(()) + } + + pub(crate) fn firmware_control(&self, output: &mut [u8]) -> Result<(), Error> { + let addresses = self.addresses()?; + let bytes = output + .get_mut(..allocation(storage::CONTROL_REGION)?.size) + .ok_or(Error::Size)?; + bytes.fill(0); + bytes[0x4f88..0x4f90].copy_from_slice(&addresses[storage::FW_CONTROL_STATE].to_le_bytes()); + bytes[0x4f90..0x4f98].copy_from_slice(&addresses[storage::FW_CONTROL_RING].to_le_bytes()); + bytes[0x4fd4..0x4fd8].copy_from_slice(&1u32.to_le_bytes()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn owners(delta: u64) -> [Region; COUNT] { + core::array::from_fn(|index| { + let item = allocation(index).unwrap(); + Region::new(item.address + delta, item.size).unwrap() + }) + } + + fn graph(owners: &[Region; COUNT]) -> PointerGraph<'_> { + PointerGraph { + owners, + scratch: Region::new(0xffff_fc22_0000_0000, 0x8000).unwrap(), + pool_alias: 0x70_0000_0000, + parameter_alias: 0x70_0000_8000, + } + } + + #[test] + fn complete_plan_has_disjoint_page_owners_and_nine_log_threads() { + let owners = owners(0); + assert!(graph(&owners).addresses().is_ok()); + assert_eq!(allocation(28).unwrap().size, 0x1b0); + assert_eq!(allocation(29).unwrap().size, 0x28800); + assert_eq!(allocation(41).unwrap().size, 0x79800); + assert!(allocation(COUNT).is_err()); + } + + #[test] + fn runtime_references_relocate_and_keep_the_packed_power_pointer() { + for delta in [0, 0x1000_0000] { + let owners = owners(delta); + let mut output = [0xa5; RUNTIME_SIZE + 8]; + graph(&owners).runtime(&mut output).unwrap(); + let read = |at| u64::from_le_bytes(output[at..at + 8].try_into().unwrap()); + assert_eq!( + read(0x469), + allocation(PERFORMANCE_CONTROL).unwrap().address + delta + ); + assert_eq!(read(0x1a0), allocation(24).unwrap().address + delta); + assert_eq!(read(0x1b8), allocation(25).unwrap().address + delta); + assert_eq!(read(0x2d0), 0x70_0000_8000); + assert_eq!(&output[RUNTIME_SIZE..], &[0xa5; 8]); + } + } + + #[test] + fn every_short_runtime_rejects_before_mutation() { + let owners = owners(0); + for length in 0..RUNTIME_SIZE { + let mut output = [0xa5; RUNTIME_SIZE]; + assert_eq!( + graph(&owners).runtime(&mut output[..length]), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } + } + + #[test] + fn every_owner_is_checked_before_any_record_is_written() { + for index in 0..COUNT { + let mut owners = owners(0); + let item = allocation(index).unwrap(); + owners[index] = Region::new(item.address, item.size - 1).unwrap(); + let mut output = [0xa5; RUNTIME_SIZE]; + assert!(graph(&owners).runtime(&mut output).is_err()); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } + let mut owners = owners(0); + owners[1] = owners[0]; + let mut output = [0xa5; RUNTIME_SIZE]; + assert!(graph(&owners).runtime(&mut output).is_err()); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_init_layout.rs b/drivers/gpu/drm/asahi/m3/m3_init_layout.rs index 92cc818e030ccb..75f06abe6f4110 100644 --- a/drivers/gpu/drm/asahi/m3/m3_init_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_init_layout.rs @@ -78,13 +78,38 @@ impl Root { (root::CONTROL, self.control.at(0, CONTROL_SIZE)?), (root::POWER, self.power.at(0, 2292)?), ]; - let bytes = out.get_mut(..ROOT_SIZE).ok_or(Error::Size)?; + Self::write_root(out, ROOT_SIZE, 0x0c08_e21e_8380_0490, &references) + } + + /// Encode the extended root without admitting a firmware image or publishing it. + pub(crate) fn encode_26(&self, out: &mut [u8], extra: Region) -> Result<(), Error> { + let references = [ + (root::REGION_A, self.region_a.at(0, 0x4000)?), + (root::RUNTIME, self.runtime.at(0, 0x4b8)?), + (root::GLOBALS, self.globals.at(0, 0xe48)?), + (root::NEW_A, self.new_a.at(0, 0x20)?), + (root::CONTROL, self.control.at(0, 0xeaf0)?), + (root::POWER, self.power.at(0, 0x670)?), + (0xc0, extra.at(0, 0x4000)?), + ]; + if references.iter().any(|(_, address)| address & 7 != 0) { + return Err(Error::Address); + } + Self::write_root(out, 0xc8, 0x0c89_c357_8392_04b8, &references) + } + + fn write_root( + out: &mut [u8], + size: usize, + version: u64, + references: &[(usize, u64)], + ) -> Result<(), Error> { + let bytes = out.get_mut(..size).ok_or(Error::Size)?; bytes.fill(0); - // Opaque qualified G15/V14_8_3 version tuple, not a semantic version. - for (i, value) in [0x490u16, 0x8380, 0xe21e, 0xc08].iter().enumerate() { - bytes[root::VERSION + i * 2..root::VERSION + i * 2 + 2].copy_from_slice(&value.to_le_bytes()); + pointer(bytes, root::VERSION, version); + for &(offset, value) in references { + pointer(bytes, offset, value); } - for (offset, value) in references { pointer(bytes, offset, value); } word(bytes, root::HOST_ALLOCATIONS, 1); bytes[root::PAGE_SIZE..root::PAGE_SIZE + 2].copy_from_slice(&0x4000u16.to_le_bytes()); bytes[root::PAGE_BITS] = 14; @@ -193,3 +218,107 @@ impl ControlRegion { Ok(()) } } + +#[cfg(test)] +mod root_format_tests { + use super::*; + + fn allocations() -> (Root, Region) { + let region = + |index: u64| Region::new(0xffff_fc01_0000_0000 + index * 0x10000, 0x10000).unwrap(); + ( + Root { + region_a: region(1), + runtime: region(2), + globals: region(3), + new_a: region(4), + control: region(5), + power: region(6), + }, + region(7), + ) + } + + #[test] + fn root_revision_changes_only_the_version_and_extension() { + let (root, extra) = allocations(); + let mut older = [0x55; 0xd0]; + let mut newer = older; + root.encode(&mut older).unwrap(); + root.encode_26(&mut newer, extra).unwrap(); + assert_eq!(&older[8..0xc0], &newer[8..0xc0]); + assert_eq!( + u64::from_le_bytes(newer[..8].try_into().unwrap()), + 0x0c89_c357_8392_04b8 + ); + assert_eq!( + u64::from_le_bytes(newer[0xc0..0xc8].try_into().unwrap()), + 0xffff_fc01_0007_0000 + ); + assert_eq!(&older[0xc0..], &[0x55; 16]); + assert_eq!(&newer[0xc8..], &[0x55; 8]); + for (offset, index) in [ + (8, 1), + (0x18, 2), + (0x20, 3), + (0xa8, 4), + (0xb0, 5), + (0xb8, 6), + ] { + assert_eq!( + u64::from_le_bytes(newer[offset..offset + 8].try_into().unwrap()), + 0xffff_fc01_0000_0000 + index * 0x10000 + ); + } + } + + #[test] + fn every_short_destination_is_unchanged() { + let (root, extra) = allocations(); + for length in 0..0xc8 { + let mut output = [0xa5; 0xc8]; + assert_eq!( + root.encode_26(&mut output[..length], extra), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; 0xc8]); + } + } + + #[test] + fn every_reference_requires_its_own_complete_allocation() { + for (index, required) in [0x4000, 0x4b8, 0xe48, 0x20, 0xeaf0, 0x670, 0x4000] + .into_iter() + .enumerate() + { + for (size, aligned, valid) in [ + (required - 1, true, false), + (required, false, false), + (required, true, true), + ] { + let (mut root, mut extra) = allocations(); + let reference = match index { + 0 => &mut root.region_a, + 1 => &mut root.runtime, + 2 => &mut root.globals, + 3 => &mut root.new_a, + 4 => &mut root.control, + 5 => &mut root.power, + _ => &mut extra, + }; + *reference = + Region::new(0xffff_fc02_0000_0000 + u64::from(!aligned), size).unwrap(); + let mut output = [0xa5; 0xc8]; + let result = root.encode_26(&mut output, extra); + assert_eq!( + result.is_ok(), + valid, + "reference {index}, size {size}, aligned {aligned}" + ); + if !valid { + assert_eq!(output, [0xa5; 0xc8]); + } + } + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs new file mode 100644 index 00000000000000..e4008f239c4a0d --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! T8122 firmware MMIO descriptors. Mapping ownership stays with the M3 runtime. + +use super::m3_init_layout::{Error, Region}; + +pub(crate) const SLOTS: usize = 53; +pub(crate) const RECORD_BYTES: usize = 40; +pub(crate) const TABLE_OFFSET: usize = 0x640; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Area { + pub(crate) physical: u64, + pub(crate) bytes: usize, + relative_register: u64, + writable: bool, + split: bool, +} + +impl Area { + pub(crate) fn page_offset(self) -> usize { + (self.physical & 0x3fff) as usize + } + + pub(crate) fn mapped_bytes(self) -> usize { + (self.page_offset() + self.bytes + 0x3fff) & !0x3fff + } + + /// At most two physical banks; the second bank occupies the next virtual pages. + pub(crate) fn banks(self) -> [(u64, usize, usize); 2] { + if self.split { + [ + (self.physical, self.bytes / 2, 0), + (self.physical + 0x200_0000, self.bytes / 2, self.bytes / 2), + ] + } else { + [(self.physical & !0x3fff, self.mapped_bytes(), 0), (0, 0, 0)] + } + } +} + +/// Register resources obtained from the admitted T8122 hardware configuration. +/// Their physical addresses are supplied by the owner, not duplicated in a wire table. +#[derive(Clone, Copy)] +pub(crate) struct RegisterBases { + pub(crate) gpu: u64, + pub(crate) interrupt: u64, + pub(crate) memory: u64, + pub(crate) telemetry_write: u64, + pub(crate) telemetry_read: u64, + pub(crate) neural: u64, + pub(crate) metrology: u64, +} + +/// Select the wire slot and map its logical hardware block independently of its VA. +pub(crate) fn area(slot: usize, bases: RegisterBases) -> Result, Error> { + let (base, offset, bytes, tagged, writable, split) = match slot { + 0 => (bases.interrupt, 0, 0x4000, false, true, false), + 3 => (bases.memory, 0, 0x30000, false, true, true), + 9 => (bases.telemetry_write, 0, 0x1000, false, true, false), + 10 => (bases.telemetry_read, 0, 0x2000, false, false, false), + 12 => (bases.neural, 0, 0x4000, false, true, false), + 14 => (bases.metrology, 0, 0x8000, false, false, false), + 17 => (bases.gpu, 0, 0x20000, false, true, false), + 26 => (bases.gpu, 0xd04000, 0x8000, true, true, false), + 27 => (bases.gpu, 0xd0d000, 0x1000, true, true, false), + 28 => (bases.gpu, 0xd50000, 0x8000, true, true, false), + 29 => (bases.gpu, 0xd10000, 0x4000, true, true, false), + 31 => (bases.gpu, 0xd40000, 0x4000, true, true, false), + 32 => (bases.gpu, 0xd60000, 0x8000, true, true, false), + 35 => (bases.gpu, 0xe00000, 0x4000, true, true, false), + 39 => (bases.gpu, 0xe08000, 0x8000, false, true, false), + 40 => (bases.gpu, 0xe1c000, 0x4000, true, true, false), + 0..SLOTS => return Ok(None), + _ => return Err(Error::Bounds), + }; + let physical = base.checked_add(offset).ok_or(Error::Address)?; + if physical == 0 || physical.checked_add(bytes as u64).ok_or(Error::Address)? > 1 << 42 { + return Err(Error::Address); + } + Ok(Some(Area { + physical, + bytes, + relative_register: if tagged { offset } else { 0 }, + writable, + split, + })) +} + +fn empty_mode(slot: usize) -> u64 { + if matches!(slot, 2 | 5..=8 | 30 | 33 | 34 | 37 | 38 | 41..=43 | 46..=49 | 52) { + 2 + } else { + 0 + } +} + +/// Encode a complete record after checking the owned mapping; errors preserve all output. +pub(crate) fn encode( + slot: usize, + bases: RegisterBases, + mapping: Option, + output: &mut [u8], +) -> Result<(), Error> { + let descriptor = area(slot, bases)?; + let (physical, va, bytes, requested, register, mode) = match (descriptor, mapping) { + (None, None) => (0, 0, 0, 0, 0, empty_mode(slot)), + (Some(item), Some(mapping)) => { + let page = mapping.at(0, item.mapped_bytes())?; + if page & 0x3fff != 0 { + return Err(Error::Address); + } + let va = page + .checked_add(item.page_offset() as u64) + .ok_or(Error::Address)?; + ( + item.physical, + va, + item.bytes as u32, + if item.split { + (item.bytes / 2) as u32 + } else { + item.bytes as u32 + }, + item.relative_register, + if item.writable { 2 } else { 0 }, + ) + } + _ => return Err(Error::Address), + }; + let bytes_out = output.get_mut(..RECORD_BYTES).ok_or(Error::Size)?; + bytes_out.fill(0); + bytes_out[0..8].copy_from_slice(&physical.to_le_bytes()); + bytes_out[8..16].copy_from_slice(&va.to_le_bytes()); + bytes_out[16..20].copy_from_slice(&bytes.to_le_bytes()); + bytes_out[20..24].copy_from_slice(&requested.to_le_bytes()); + bytes_out[24..32].copy_from_slice(®ister.to_le_bytes()); + bytes_out[32..40].copy_from_slice(&mode.to_le_bytes()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Register identities already present in the public T8122 configuration; + // memory-controller bank geometry is shared with the public 25G83 encoder. + const BASES: RegisterBases = RegisterBases { + gpu: 0x2_9000_0000, + interrupt: 0x2_d101_4000, + memory: 0x2_201c_4000, + telemetry_write: 0x2_d03d_0000, + telemetry_read: 0x2_d03c_0000, + neural: 0x3_1145_c000, + metrology: 0x2_d028_0000, + }; + + #[test] + fn j613_slot28_bank_and_wire_tag_agree() { + let bank = area(28, BASES).unwrap().unwrap(); + assert_eq!(bank.physical, BASES.gpu + 0xd50000); + assert_eq!(bank.bytes, 0x8000); + let mapped = Region::new(0xffff_fc23_01c0_0000, 0x8000).unwrap(); + let mut bytes = [0; RECORD_BYTES]; + encode(28, BASES, Some(mapped), &mut bytes).unwrap(); + assert_eq!(&bytes[0..8], &(BASES.gpu + 0xd50000).to_le_bytes()); + assert_eq!(&bytes[24..32], &0xd50000u64.to_le_bytes()); + assert_ne!(bank.physical, BASES.gpu + 0xd58000); + } + + #[test] + fn descriptor_inventory_ends_before_the_retention_pointer() { + assert_eq!(TABLE_OFFSET + SLOTS * RECORD_BYTES, 0xe88); + assert_eq!( + (0..SLOTS) + .filter(|slot| area(*slot, BASES).unwrap().is_some()) + .count(), + 16 + ); + assert!(area(SLOTS, BASES).is_err()); + } + + #[test] + fn discontiguous_memory_controller_banks_have_distinct_physical_owners() { + let item = area(3, BASES).unwrap().unwrap(); + assert_eq!( + item.banks(), + [ + (0x2_201c_4000, 0x18000, 0), + (0x2_221c_4000, 0x18000, 0x18000) + ] + ); + let mapping = Region::new(0xffff_fc23_0000_0000, 0x30000).unwrap(); + let mut bytes = [0xa5; RECORD_BYTES]; + encode(3, BASES, Some(mapping), &mut bytes).unwrap(); + assert_eq!( + u32::from_le_bytes(bytes[16..20].try_into().unwrap()), + 0x30000 + ); + assert_eq!( + u32::from_le_bytes(bytes[20..24].try_into().unwrap()), + 0x18000 + ); + } + + #[test] + fn subpage_mapping_keeps_the_register_offset() { + let item = area(27, BASES).unwrap().unwrap(); + assert_eq!(item.page_offset(), 0x1000); + assert_eq!(item.banks()[0], (0x2_90d0_c000, 0x4000, 0)); + let mut output = [0xa5; RECORD_BYTES + 8]; + encode( + 27, + BASES, + Some(Region::new(0xffff_fc23_0000_0000, 0x4000).unwrap()), + &mut output, + ) + .unwrap(); + assert_eq!( + u64::from_le_bytes(output[8..16].try_into().unwrap()), + 0xffff_fc23_0000_1000 + ); + assert_eq!(&output[RECORD_BYTES..], &[0xa5; 8]); + } + + #[test] + fn empty_slots_keep_modes_without_fabricating_mappings() { + for slot in 0..SLOTS { + if area(slot, BASES).unwrap().is_some() { + continue; + } + let mut output = [0xa5; RECORD_BYTES]; + encode(slot, BASES, None, &mut output).unwrap(); + assert_eq!(&output[..32], &[0; 32]); + assert_eq!( + u64::from_le_bytes(output[32..40].try_into().unwrap()), + empty_mode(slot) + ); + } + } + + #[test] + fn invalid_mapping_or_output_never_partially_changes_a_descriptor() { + let good = Region::new(0xffff_fc23_0000_0000, 0x4000).unwrap(); + for (slot, mapping) in [ + (SLOTS, None), + (0, None), + (1, Some(good)), + (0, Some(Region::new(0xffff_fc23_0000_0001, 0x4000).unwrap())), + (0, Some(Region::new(0xffff_fc23_0000_0000, 0x3fff).unwrap())), + ] { + let mut output = [0xa5; RECORD_BYTES]; + assert!(encode(slot, BASES, mapping, &mut output).is_err()); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + for length in 0..RECORD_BYTES { + let mut output = [0xa5; RECORD_BYTES]; + assert_eq!( + encode(0, BASES, Some(good), &mut output[..length]), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_profile.rs b/drivers/gpu/drm/asahi/m3/m3_profile.rs new file mode 100644 index 00000000000000..c70a88029072b6 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_profile.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Firmware-interface properties, independent of the physical G15 GPU record format. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Profile { + Legacy, + Extended, +} + +pub(crate) const PARAMS_REQUIRED: u64 = 600; + +impl Profile { + pub(crate) const fn firmware_compat(self) -> [u32; 3] { + match self { + Self::Legacy => [14, 8, 3], + Self::Extended => [26, 6, 2], + } + } + + pub(crate) const fn hal(self) -> u32 { + match self { + Self::Legacy => 0, + Self::Extended => 200, + } + } + + pub(crate) const fn user_end(self) -> u64 { + match self { + Self::Legacy => 0x2ff_ffff_8000, + Self::Extended => (1 << 42) - 2 * 0x4000, + } + } + + pub(crate) const fn vm_acknowledged(self, params_bytes: u64, flags: u32) -> bool { + // Both profiles consume G15 records; the existing VM padding stays zero. + params_bytes >= PARAMS_REQUIRED && flags == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profile_acknowledgement_is_exact_and_file_query_covers_hal() { + for profile in [Profile::Legacy, Profile::Extended] { + let expected = 0; + for size in [0, 599, 600, 616, u64::MAX] { + for flags in [0, 1, 2, 3, u32::MAX] { + assert_eq!( + profile.vm_acknowledged(size, flags), + size >= 600 && flags == expected + ); + } + } + } + } + + #[test] + fn firmware_interface_version_does_not_change_physical_identity() { + assert_eq!(Profile::Legacy.hal(), 0); + assert_eq!(Profile::Extended.hal(), 200); + assert_eq!(Profile::Legacy.firmware_compat(), [14, 8, 3]); + assert_eq!(Profile::Extended.firmware_compat(), [26, 6, 2]); + assert_eq!(Profile::Extended.user_end(), 0x3ff_ffff_8000); + assert_eq!(Profile::Legacy.user_end(), 0x2ff_ffff_8000); + } +} diff --git a/drivers/gpu/drm/asahi/m3/mod.rs b/drivers/gpu/drm/asahi/m3/mod.rs index 1d10657c02f662..7073ecf294d631 100644 --- a/drivers/gpu/drm/asahi/m3/mod.rs +++ b/drivers/gpu/drm/asahi/m3/mod.rs @@ -198,3 +198,7 @@ mod t8122_admission; pub(crate) mod m3_soc; mod t8122_hwdata; + +mod m3_init_26; +mod m3_profile; +mod m3_mmio_26; diff --git a/drivers/gpu/drm/asahi/m3/t8122_admission.rs b/drivers/gpu/drm/asahi/m3/t8122_admission.rs index 7505065810acc3..c66ccb72cd18d7 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_admission.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_admission.rs @@ -2,6 +2,8 @@ //! Read-only T8122 identity admission. Firmware startup remains disabled. +use super::m3_profile::Profile; + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) struct Identity { pub(crate) active_cores: u32, @@ -16,12 +18,23 @@ pub(crate) fn validate_identity( result: u32, id_version: u32, masks: [u32; 3], +) -> Option { + validate_identity_for(Profile::Legacy, firmware_compat, schema, result, id_version, masks) +} + +pub(crate) fn validate_identity_for( + profile: Profile, + firmware_compat: &[u32], + schema: u32, + result: u32, + id_version: u32, + masks: [u32; 3], ) -> Option { let family = id_version >> 24; let variant = (id_version >> 16) & 0xff; let revision = (id_version >> 8) & 0xff; let active_cores = masks[0].count_ones(); - if firmware_compat != [14, 8, 3] + if firmware_compat != profile.firmware_compat() || schema != 1 || result != 1 || family != 7 @@ -94,6 +107,14 @@ pub(crate) fn opp_table_shape(opps: impl IntoIterator) -> Opt #[cfg(not(test))] pub(crate) fn read_identity( pdev: &kernel::platform::Device>, +) -> kernel::error::Result { + read_identity_for(pdev, Profile::Legacy) +} + +#[cfg(not(test))] +pub(crate) fn read_identity_for( + pdev: &kernel::platform::Device>, + profile: Profile, ) -> kernel::error::Result { use kernel::{bindings, c_str, prelude::*}; @@ -146,7 +167,8 @@ pub(crate) fn read_identity( observation.get_u32(c_str!("core-mask-1"))?, observation.get_u32(c_str!("core-mask-2"))?, ]; - validate_identity(firmware_compat.as_slice(), schema, result, id_version, masks).ok_or(ENODEV) + validate_identity_for(profile, firmware_compat.as_slice(), schema, result, id_version, masks) + .ok_or(ENODEV) } #[cfg(test)] @@ -164,6 +186,29 @@ mod tests { } } + #[test] + fn extended_identity_requires_its_actual_version_and_the_same_g15g_bounds() { + for mask in [0xff, 0x3ff] { + let good = || validate_identity_for(Profile::Extended, &[26, 6, 2], 1, 1, G15G_C0, [mask, 0, 0]); + assert_eq!(good().unwrap().active_cores, mask.count_ones()); + assert!(validate_identity(&[26, 6, 2], 1, 1, G15G_C0, [mask, 0, 0]).is_none()); + assert!(validate_identity_for(Profile::Extended, &[14, 8, 3], 1, 1, G15G_C0, [mask, 0, 0]).is_none()); + } + for (schema, result, id, masks) in [ + (0, 1, G15G_C0, [0xff, 0, 0]), + (1, 0, G15G_C0, [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 24), [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 16), [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 8), [0xff, 0, 0]), + (1, 1, G15G_C0, [0x1ff, 0, 0]), + (1, 1, G15G_C0, [0x7ff, 0, 0]), + (1, 1, G15G_C0, [0xff, 1, 0]), + (1, 1, G15G_C0, [0xff, 0, 1]), + ] { + assert!(validate_identity_for(Profile::Extended, &[26, 6, 2], schema, result, id, masks).is_none()); + } + } + /// The booted J613 ladder (`perf-states`, frequency then millivolts), in ADT order: fourteen /// states, eight voltages above the idle one, 1338 MHz at the top. const J613_PERF_STATES: [(u64, u32); 14] = [ diff --git a/tools/asahi/m3-firmware-tests.rs b/tools/asahi/m3-firmware-tests.rs new file mode 100644 index 00000000000000..90539441d623ed --- /dev/null +++ b/tools/asahi/m3-firmware-tests.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! Host tests of production firmware identity and normalization. +#![allow(dead_code)] +#[path = "../../drivers/gpu/drm/asahi/m3/m3_profile.rs"] +pub(crate) mod m3_profile; +#[path = "../../drivers/gpu/drm/asahi/m3/agx_resources.rs"] +pub(crate) mod resources; +mod m3 { + pub(crate) use crate::firmware as m3_firmware; + pub(crate) use crate::m3_profile; + pub(crate) use crate::resources as m3_resources; +} +#[path = "../../drivers/gpu/drm/asahi/m3/m3_firmware.rs"] +pub(crate) mod firmware; diff --git a/tools/asahi/m3-runtime-tests.rs b/tools/asahi/m3-runtime-tests.rs index fd4dc90489e817..f1768a242147b6 100644 --- a/tools/asahi/m3-runtime-tests.rs +++ b/tools/asahi/m3-runtime-tests.rs @@ -15,3 +15,12 @@ mod m3_init_storage; mod m3_thermal_policy; #[path = "../../drivers/gpu/drm/asahi/m3/t8122_hwdata.rs"] mod t8122_hwdata; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_init_26.rs"] +mod m3_init_26; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_profile.rs"] +mod m3_profile; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_mmio_26.rs"] +mod m3_mmio_26; diff --git a/tools/asahi/m3-uapi-fixture.c b/tools/asahi/m3-uapi-fixture.c new file mode 100644 index 00000000000000..9ee09940960093 --- /dev/null +++ b/tools/asahi/m3-uapi-fixture.c @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include + +#ifndef ASAHI_HEADER +#define ASAHI_HEADER "drm/asahi_drm.h" +#endif +#include ASAHI_HEADER + +static_assert(sizeof(struct drm_asahi_cmd_header) == 8, "command header"); +static_assert(sizeof(struct drm_asahi_attachment) == 24, "attachment"); +static_assert(sizeof(struct drm_asahi_timestamps) == 16, "timestamps"); +static_assert(offsetof(struct drm_asahi_cmd_compute, ts_end_copies) == 64, + "compute prefix"); +static_assert(offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies) == 256, + "render prefix"); + +static int emit(unsigned int type, const void *payload, size_t size) +{ + struct drm_asahi_cmd_header header = { + .cmd_type = type, + .size = size, + .vdm_barrier = DRM_ASAHI_BARRIER_NONE, + .cdm_barrier = DRM_ASAHI_BARRIER_NONE, + }; + + return fwrite(&header, sizeof(header), 1, stdout) != 1 || + fwrite(payload, size, 1, stdout) != 1; +} + +/* Named C offsets are exported independently of the Rust decoder. */ +static void layout(void) +{ + printf("profile.params_required %zu 0\n", + offsetof(struct drm_asahi_params_global, gpu_hal_generation) + sizeof(__u32)); + printf("profile.vm_create_size %zu 0\n", sizeof(struct drm_asahi_vm_create)); + printf("profile.hal200 %u 0\n", (unsigned int)DRM_ASAHI_GPU_HAL_200); + printf("profile.features %u 0\n", + DRM_ASAHI_FEATURE_VM_STATUS | DRM_ASAHI_FEATURE_SCHEDULED_QUEUES); + printf("header %zu 0\n", sizeof(struct drm_asahi_cmd_header)); + printf("attachment %zu 0\n", sizeof(struct drm_asahi_attachment)); + printf("timestamps %zu 0\n", sizeof(struct drm_asahi_timestamps)); + printf("cmd_render.flags %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, flags), + sizeof(((struct drm_asahi_cmd_render *)0)->flags)); + printf("cmd_render.isp_zls_pixels %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_zls_pixels), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_zls_pixels)); + printf("cmd_render.vdm_ctrl_stream_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vdm_ctrl_stream_base), + sizeof(((struct drm_asahi_cmd_render *)0)->vdm_ctrl_stream_base)); + printf("cmd_render.vertex_helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.binary), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.binary)); + printf("cmd_render.vertex_helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.cfg), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.cfg)); + printf("cmd_render.vertex_helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.data), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.data)); + printf("cmd_render.fragment_helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.binary), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.binary)); + printf("cmd_render.fragment_helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.cfg), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.cfg)); + printf("cmd_render.fragment_helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.data), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.data)); + printf("cmd_render.isp_scissor_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_scissor_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_scissor_base)); + printf("cmd_render.isp_dbias_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_dbias_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_dbias_base)); + printf("cmd_render.isp_oclqry_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_oclqry_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_oclqry_base)); + printf("cmd_render.depth.base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.base), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.base)); + printf("cmd_render.depth.comp_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.comp_base), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.comp_base)); + printf("cmd_render.depth.stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.stride), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.stride)); + printf("cmd_render.depth.comp_stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.comp_stride), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.comp_stride)); + printf("cmd_render.stencil.base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.base), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.base)); + printf("cmd_render.stencil.comp_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.comp_base), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.comp_base)); + printf("cmd_render.stencil.stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.stride), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.stride)); + printf("cmd_render.stencil.comp_stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.comp_stride), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.comp_stride)); + printf("cmd_render.zls_ctrl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, zls_ctrl), + sizeof(((struct drm_asahi_cmd_render *)0)->zls_ctrl)); + printf("cmd_render.ppp_multisamplectl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ppp_multisamplectl), + sizeof(((struct drm_asahi_cmd_render *)0)->ppp_multisamplectl)); + printf("cmd_render.sampler_heap %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sampler_heap), + sizeof(((struct drm_asahi_cmd_render *)0)->sampler_heap)); + printf("cmd_render.ppp_ctrl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ppp_ctrl), + sizeof(((struct drm_asahi_cmd_render *)0)->ppp_ctrl)); + printf("cmd_render.width_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, width_px), + sizeof(((struct drm_asahi_cmd_render *)0)->width_px)); + printf("cmd_render.height_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, height_px), + sizeof(((struct drm_asahi_cmd_render *)0)->height_px)); + printf("cmd_render.layers %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, layers), + sizeof(((struct drm_asahi_cmd_render *)0)->layers)); + printf("cmd_render.sampler_count %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sampler_count), + sizeof(((struct drm_asahi_cmd_render *)0)->sampler_count)); + printf("cmd_render.utile_width_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, utile_width_px), + sizeof(((struct drm_asahi_cmd_render *)0)->utile_width_px)); + printf("cmd_render.utile_height_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, utile_height_px), + sizeof(((struct drm_asahi_cmd_render *)0)->utile_height_px)); + printf("cmd_render.samples %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, samples), + sizeof(((struct drm_asahi_cmd_render *)0)->samples)); + printf("cmd_render.sample_size_B %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sample_size_B), + sizeof(((struct drm_asahi_cmd_render *)0)->sample_size_B)); + printf("cmd_render.isp_merge_upper_x %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_merge_upper_x), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_merge_upper_x)); + printf("cmd_render.isp_merge_upper_y %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_merge_upper_y), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_merge_upper_y)); + printf("cmd_render.bg.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->bg.usc)); + printf("cmd_render.bg.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->bg.rsrc_spec)); + printf("cmd_render.bg_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_rsrc_spec_hi)); + printf("cmd_render.eot.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, eot.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->eot.usc)); + printf("cmd_render.eot.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, eot.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->eot.rsrc_spec)); + printf("cmd_render.bg_eot_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_eot_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_eot_rsrc_spec_hi)); + printf("cmd_render.partial_bg.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_bg.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_bg.usc)); + printf("cmd_render.partial_bg.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_bg.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_bg.rsrc_spec)); + printf("cmd_render.bg_partial_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_partial_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_partial_rsrc_spec_hi)); + printf("cmd_render.partial_eot.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_eot.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_eot.usc)); + printf("cmd_render.partial_eot.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_eot.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_eot.rsrc_spec)); + printf("cmd_render.bg_eot_partial_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_eot_partial_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_eot_partial_rsrc_spec_hi)); + printf("cmd_render.isp_bgobjdepth %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_bgobjdepth), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_bgobjdepth)); + printf("cmd_render.isp_bgobjvals %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_bgobjvals), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_bgobjvals)); + printf("cmd_render.ts_vtx.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.start.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.start.handle)); + printf("cmd_render.ts_vtx.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.start.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.start.offset)); + printf("cmd_render.ts_vtx.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.end.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.end.handle)); + printf("cmd_render.ts_vtx.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.end.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.end.offset)); + printf("cmd_render.ts_frag.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.start.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.start.handle)); + printf("cmd_render.ts_frag.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.start.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.start.offset)); + printf("cmd_render.ts_frag.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.end.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.end.handle)); + printf("cmd_render.ts_frag.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.end.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.end.offset)); + printf("cmd_compute.flags %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, flags), + sizeof(((struct drm_asahi_cmd_compute *)0)->flags)); + printf("cmd_compute.sampler_count %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, sampler_count), + sizeof(((struct drm_asahi_cmd_compute *)0)->sampler_count)); + printf("cmd_compute.cdm_ctrl_stream_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, cdm_ctrl_stream_base), + sizeof(((struct drm_asahi_cmd_compute *)0)->cdm_ctrl_stream_base)); + printf("cmd_compute.cdm_ctrl_stream_end %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, cdm_ctrl_stream_end), + sizeof(((struct drm_asahi_cmd_compute *)0)->cdm_ctrl_stream_end)); + printf("cmd_compute.sampler_heap %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, sampler_heap), + sizeof(((struct drm_asahi_cmd_compute *)0)->sampler_heap)); + printf("cmd_compute.helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.binary), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.binary)); + printf("cmd_compute.helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.cfg), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.cfg)); + printf("cmd_compute.helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.data), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.data)); + printf("cmd_compute.ts.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.start.handle), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.start.handle)); + printf("cmd_compute.ts.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.start.offset), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.start.offset)); + printf("cmd_compute.ts.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.end.handle), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.end.handle)); + printf("cmd_compute.ts.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.end.offset), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.end.offset)); + printf("attachment.pointer %zu %zu\n", + offsetof(struct drm_asahi_attachment, pointer), + sizeof(((struct drm_asahi_attachment *)0)->pointer)); + printf("attachment.size %zu %zu\n", + offsetof(struct drm_asahi_attachment, size), + sizeof(((struct drm_asahi_attachment *)0)->size)); + printf("cmd_render.ts_frag_end_copies %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag_end_copies)); + printf("cmd_compute.ts_end_copies %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts_end_copies), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts_end_copies)); +} + +int main(int argc, char **argv) +{ + struct drm_asahi_attachment attachment = {0}; + + attachment.pointer = 0xcdb09376593c1f02ULL; + attachment.size = 0xceb194775a3d2003ULL; + + struct drm_asahi_cmd_render render = {0}; + + render.flags = 0x5b3e2104; + render.isp_zls_pixels = 0x5c3f2205; + render.vdm_ctrl_stream_base = 0xd1b4977a5d402306ULL; + render.vertex_helper.binary = 0x5e412407; + render.vertex_helper.cfg = 0x5f422508; + render.vertex_helper.data = 0xd4b79a7d60432609ULL; + render.fragment_helper.binary = 0x6144270a; + render.fragment_helper.cfg = 0x6245280b; + render.fragment_helper.data = 0xd7ba9d806346290cULL; + render.isp_scissor_base = 0xd8bb9e8164472a0dULL; + render.isp_dbias_base = 0xd9bc9f8265482b0eULL; + render.isp_oclqry_base = 0xdabda08366492c0fULL; + render.depth.base = 0xdbbea184674a2d10ULL; + render.depth.comp_base = 0xdcbfa285684b2e11ULL; + render.depth.stride = 0x694c2f12; + render.depth.comp_stride = 0x6a4d3013; + render.stencil.base = 0xdfc2a5886b4e3114ULL; + render.stencil.comp_base = 0xe0c3a6896c4f3215ULL; + render.stencil.stride = 0x6d503316; + render.stencil.comp_stride = 0x6e513417; + render.zls_ctrl = 0xe3c6a98c6f523518ULL; + render.ppp_multisamplectl = 0xe4c7aa8d70533619ULL; + render.sampler_heap = 0xe5c8ab8e7154371aULL; + render.ppp_ctrl = 0x7255381b; + render.width_px = 0x391c; + render.height_px = 0x3a1d; + render.layers = 0x3b1e; + render.sampler_count = 0x3c1f; + render.utile_width_px = 0x20; + render.utile_height_px = 0x21; + render.samples = 0x22; + render.sample_size_B = 0x23; + render.isp_merge_upper_x = 0x7b5e4124; + render.isp_merge_upper_y = 0x7c5f4225; + render.bg.usc = 0x7d604326; + render.bg.rsrc_spec = 0x7e614427; + render.bg_rsrc_spec_hi = 0x7f624528; + render.eot.usc = 0x80634629; + render.eot.rsrc_spec = 0x8164472a; + render.bg_eot_rsrc_spec_hi = 0x8265482b; + render.partial_bg.usc = 0x8366492c; + render.partial_bg.rsrc_spec = 0x84674a2d; + render.bg_partial_rsrc_spec_hi = 0x85684b2e; + render.partial_eot.usc = 0x86694c2f; + render.partial_eot.rsrc_spec = 0x876a4d30; + render.bg_eot_partial_rsrc_spec_hi = 0x886b4e31; + render.isp_bgobjdepth = 0x896c4f32; + render.isp_bgobjvals = 0x8a6d5033; + render.ts_vtx.start.handle = 0x8b6e5134; + render.ts_vtx.start.offset = 0x8c6f5235; + render.ts_vtx.end.handle = 0x8d705336; + render.ts_vtx.end.offset = 0x8e715437; + render.ts_frag.start.handle = 0x8f725538; + render.ts_frag.start.offset = 0x90735639; + render.ts_frag.end.handle = 0x9174573a; + render.ts_frag.end.offset = 0x9275583b; + + struct drm_asahi_cmd_compute compute = {0}; + + compute.flags = 0x9376593c; + compute.sampler_count = 0x94775a3d; + compute.cdm_ctrl_stream_base = 0xbeccfb295785b3eULL; + compute.cdm_ctrl_stream_end = 0xcedd0b396795c3fULL; + compute.sampler_heap = 0xdeed1b4977a5d40ULL; + compute.helper.binary = 0x987b5e41; + compute.helper.cfg = 0x997c5f42; + compute.helper.data = 0x10f1d4b79a7d6043ULL; + compute.ts.start.handle = 0x9b7e6144; + compute.ts.start.offset = 0x9c7f6245; + compute.ts.end.handle = 0x9d806346; + compute.ts.end.offset = 0x9e816447; + + size_t render_size = + offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies); + size_t compute_size = + offsetof(struct drm_asahi_cmd_compute, ts_end_copies); + + if (argc == 2 && !strcmp(argv[1], "--layout")) { + layout(); + return 0; + } else if (argc == 2 && !strcmp(argv[1], "--full")) { + render_size = sizeof(render); + compute_size = sizeof(compute); + } else if (argc != 1) { + return 1; + } + + return emit(DRM_ASAHI_SET_VERTEX_ATTACHMENTS, &attachment, + sizeof(attachment)) || + emit(DRM_ASAHI_CMD_RENDER, &render, render_size) || + emit(DRM_ASAHI_CMD_COMPUTE, &compute, compute_size); +} diff --git a/tools/asahi/m3-uapi-tests.rs b/tools/asahi/m3-uapi-tests.rs new file mode 100644 index 00000000000000..4dc6012f5213e7 --- /dev/null +++ b/tools/asahi/m3-uapi-tests.rs @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! Offline tests of the production parser. All addresses are synthetic. +#![allow(dead_code)] + +#[path = "../../drivers/gpu/drm/asahi/m3/t8122_admission.rs"] +mod admission; +#[path = "../../drivers/gpu/drm/asahi/m3/m3_profile.rs"] +mod m3_profile; +#[path = "../../drivers/gpu/drm/asahi/m3/agx_uapi.rs"] +mod uapi; +use uapi::*; + +fn frame(kind: u16, payload: &[u8]) -> Vec { + let mut bytes = Vec::new(); + for word in [ + kind, + payload.len() as u16, + UAPI_BARRIER_NONE, + UAPI_BARRIER_NONE, + ] { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(payload); + bytes +} + +fn parse(bytes: &[u8]) -> Result { + let mut parser = UapiCommandParser::new(bytes); + while parser.next_hardware()?.is_some() {} + parser.finish() +} + +// Each offset below is checked against offsetof of the corresponding C member. +fn layout() -> &'static std::collections::BTreeMap { + static LAYOUT: std::sync::OnceLock> = + std::sync::OnceLock::new(); + LAYOUT.get_or_init(|| { + let path = std::env::var("ASAHI_UAPI_LAYOUT").expect("C layout path"); + std::fs::read_to_string(path) + .unwrap() + .lines() + .map(|line| { + let words: Vec<_> = line.split_whitespace().collect(); + ( + words[0].to_string(), + (words[1].parse().unwrap(), words[2].parse().unwrap()), + ) + }) + .collect() + }) +} + +fn field(raw: &[u8], name: &str, offset: usize, width: usize, actual: u64) { + assert_eq!(layout()[name], (offset, width), "C offset/width for {name}"); + let mut expected = [0u8; 8]; + for (i, byte) in expected[..width].iter_mut().enumerate() { + *byte = raw.get(offset + i).copied().unwrap_or(0); + } + assert_eq!(actual, u64::from_le_bytes(expected), "decoded {name}"); +} + +fn check_render(p: UapiRenderCommand, raw: &[u8]) { + field(raw, "cmd_render.flags", 0, 4, p.flags as u64); + field( + raw, + "cmd_render.isp_zls_pixels", + 4, + 4, + p.depth_dimensions as u64, + ); + field( + raw, + "cmd_render.vdm_ctrl_stream_base", + 8, + 8, + p.vdm_base as u64, + ); + field( + raw, + "cmd_render.vertex_helper.binary", + 16, + 4, + p.vertex_helper.binary as u64, + ); + field( + raw, + "cmd_render.vertex_helper.cfg", + 20, + 4, + p.vertex_helper.config as u64, + ); + field( + raw, + "cmd_render.vertex_helper.data", + 24, + 8, + p.vertex_helper.data as u64, + ); + field( + raw, + "cmd_render.fragment_helper.binary", + 32, + 4, + p.fragment_helper.binary as u64, + ); + field( + raw, + "cmd_render.fragment_helper.cfg", + 36, + 4, + p.fragment_helper.config as u64, + ); + field( + raw, + "cmd_render.fragment_helper.data", + 40, + 8, + p.fragment_helper.data as u64, + ); + field( + raw, + "cmd_render.isp_scissor_base", + 48, + 8, + p.scissor_base as u64, + ); + field( + raw, + "cmd_render.isp_dbias_base", + 56, + 8, + p.depth_bias_base as u64, + ); + field( + raw, + "cmd_render.isp_oclqry_base", + 64, + 8, + p.occlusion_query_base as u64, + ); + field(raw, "cmd_render.depth.base", 72, 8, p.depth.base as u64); + field( + raw, + "cmd_render.depth.comp_base", + 80, + 8, + p.depth.compression_base as u64, + ); + field(raw, "cmd_render.depth.stride", 88, 4, p.depth.stride as u64); + field( + raw, + "cmd_render.depth.comp_stride", + 92, + 4, + p.depth.compression_stride as u64, + ); + field(raw, "cmd_render.stencil.base", 96, 8, p.stencil.base as u64); + field( + raw, + "cmd_render.stencil.comp_base", + 104, + 8, + p.stencil.compression_base as u64, + ); + field( + raw, + "cmd_render.stencil.stride", + 112, + 4, + p.stencil.stride as u64, + ); + field( + raw, + "cmd_render.stencil.comp_stride", + 116, + 4, + p.stencil.compression_stride as u64, + ); + field(raw, "cmd_render.zls_ctrl", 120, 8, p.zls_control as u64); + field( + raw, + "cmd_render.ppp_multisamplectl", + 128, + 8, + p.multisample_control as u64, + ); + field( + raw, + "cmd_render.sampler_heap", + 136, + 8, + p.sampler_heap as u64, + ); + field(raw, "cmd_render.ppp_ctrl", 144, 4, p.ppp_control as u64); + field(raw, "cmd_render.width_px", 148, 2, p.width as u64); + field(raw, "cmd_render.height_px", 150, 2, p.height as u64); + field(raw, "cmd_render.layers", 152, 2, p.layers as u64); + field( + raw, + "cmd_render.sampler_count", + 154, + 2, + p.sampler_count as u64, + ); + field( + raw, + "cmd_render.utile_width_px", + 156, + 1, + p.utile_width as u64, + ); + field( + raw, + "cmd_render.utile_height_px", + 157, + 1, + p.utile_height as u64, + ); + field(raw, "cmd_render.samples", 158, 1, p.samples as u64); + field( + raw, + "cmd_render.sample_size_B", + 159, + 1, + p.sample_size as u64, + ); + field( + raw, + "cmd_render.isp_merge_upper_x", + 160, + 4, + p.merge_upper_x as u64, + ); + field( + raw, + "cmd_render.isp_merge_upper_y", + 164, + 4, + p.merge_upper_y as u64, + ); + field(raw, "cmd_render.bg.usc", 168, 4, p.background.usc as u64); + field( + raw, + "cmd_render.bg.rsrc_spec", + 172, + 4, + (p.background.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_rsrc_spec_hi", + 248, + 4, + (p.background.resource_spec >> 32) as u64, + ); + field(raw, "cmd_render.eot.usc", 176, 4, p.end_of_tile.usc as u64); + field( + raw, + "cmd_render.eot.rsrc_spec", + 180, + 4, + (p.end_of_tile.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_eot_rsrc_spec_hi", + 240, + 4, + (p.end_of_tile.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.partial_bg.usc", + 184, + 4, + p.partial_background.usc as u64, + ); + field( + raw, + "cmd_render.partial_bg.rsrc_spec", + 188, + 4, + (p.partial_background.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_partial_rsrc_spec_hi", + 252, + 4, + (p.partial_background.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.partial_eot.usc", + 192, + 4, + p.partial_end_of_tile.usc as u64, + ); + field( + raw, + "cmd_render.partial_eot.rsrc_spec", + 196, + 4, + (p.partial_end_of_tile.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_eot_partial_rsrc_spec_hi", + 244, + 4, + (p.partial_end_of_tile.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.isp_bgobjdepth", + 200, + 4, + p.depth_clear as u64, + ); + field( + raw, + "cmd_render.isp_bgobjvals", + 204, + 4, + p.stencil_clear as u64, + ); + field( + raw, + "cmd_render.ts_vtx.start.handle", + 208, + 4, + p.vertex_timestamps.start.handle as u64, + ); + field( + raw, + "cmd_render.ts_vtx.start.offset", + 212, + 4, + p.vertex_timestamps.start.offset as u64, + ); + field( + raw, + "cmd_render.ts_vtx.end.handle", + 216, + 4, + p.vertex_timestamps.end.handle as u64, + ); + field( + raw, + "cmd_render.ts_vtx.end.offset", + 220, + 4, + p.vertex_timestamps.end.offset as u64, + ); + field( + raw, + "cmd_render.ts_frag.start.handle", + 224, + 4, + p.fragment_timestamps.start.handle as u64, + ); + field( + raw, + "cmd_render.ts_frag.start.offset", + 228, + 4, + p.fragment_timestamps.start.offset as u64, + ); + field( + raw, + "cmd_render.ts_frag.end.handle", + 232, + 4, + p.fragment_timestamps.end.handle as u64, + ); + field( + raw, + "cmd_render.ts_frag.end.offset", + 236, + 4, + p.fragment_timestamps.end.offset as u64, + ); +} + +fn check_compute(p: UapiComputeCommand, raw: &[u8]) { + field(raw, "cmd_compute.flags", 0, 4, p.flags as u64); + field( + raw, + "cmd_compute.sampler_count", + 4, + 4, + p.sampler_count as u64, + ); + field( + raw, + "cmd_compute.cdm_ctrl_stream_base", + 8, + 8, + p.control_stream_base as u64, + ); + field( + raw, + "cmd_compute.cdm_ctrl_stream_end", + 16, + 8, + p.control_stream_end as u64, + ); + field( + raw, + "cmd_compute.sampler_heap", + 24, + 8, + p.sampler_heap as u64, + ); + field( + raw, + "cmd_compute.helper.binary", + 32, + 4, + p.helper.binary as u64, + ); + field(raw, "cmd_compute.helper.cfg", 36, 4, p.helper.config as u64); + field(raw, "cmd_compute.helper.data", 40, 8, p.helper.data as u64); + field( + raw, + "cmd_compute.ts.start.handle", + 48, + 4, + p.timestamps.start.handle as u64, + ); + field( + raw, + "cmd_compute.ts.start.offset", + 52, + 4, + p.timestamps.start.offset as u64, + ); + field( + raw, + "cmd_compute.ts.end.handle", + 56, + 4, + p.timestamps.end.handle as u64, + ); + field( + raw, + "cmd_compute.ts.end.offset", + 60, + 4, + p.timestamps.end.offset as u64, + ); +} + +fn fixture() -> Vec { + std::fs::read(std::env::var("ASAHI_UAPI_FIXTURE").expect("C fixture path")).unwrap() +} + +#[test] +fn c_header_producer_matches_production_decoder() { + let offsets = layout(); + assert_eq!(offsets["header"].0, UAPI_COMMAND_HEADER_SIZE); + assert_eq!(offsets["attachment"].0, UAPI_ATTACHMENT_SIZE); + assert_eq!(offsets["timestamps"].0, 16); + assert_eq!(offsets["cmd_render.ts_frag_end_copies"].0, UAPI_RENDER_SIZE); + assert_eq!(offsets["cmd_compute.ts_end_copies"].0, UAPI_COMPUTE_SIZE); + let bytes = fixture(); + let render_offset = 2 * UAPI_COMMAND_HEADER_SIZE + UAPI_ATTACHMENT_SIZE; + let render_size = u16::from_le_bytes( + bytes[render_offset - 6..render_offset - 4] + .try_into() + .unwrap(), + ) as usize; + let compute_offset = render_offset + render_size + UAPI_COMMAND_HEADER_SIZE; + let mut parser = UapiCommandParser::new(&bytes); + let Some(ParsedHardwareCommand::Render { + index, + header, + payload, + vertex_attachments, + fragment_attachments, + }) = parser.next_hardware().unwrap() + else { + panic!("render missing") + }; + assert_eq!(index, 2); + assert_eq!( + header, + UapiCommandHeader { + command_type: 0, + size: render_size as u16, + render_barrier: UAPI_BARRIER_NONE, + compute_barrier: UAPI_BARRIER_NONE, + } + ); + check_render(payload, &bytes[render_offset..render_offset + render_size]); + assert_eq!(vertex_attachments.count, 1); + let attachment = vertex_attachments.as_slice()[0]; + field( + &bytes[8..32], + "attachment.pointer", + 0, + 8, + attachment.address, + ); + field(&bytes[8..32], "attachment.size", 8, 8, attachment.size); + assert!(fragment_attachments.as_slice().is_empty()); + let Some(ParsedHardwareCommand::Compute { + index, + header, + payload, + attachments, + }) = parser.next_hardware().unwrap() + else { + panic!("compute missing") + }; + assert_eq!(index, 3); + assert_eq!( + header, + UapiCommandHeader { + command_type: 1, + size: (bytes.len() - compute_offset) as u16, + render_barrier: UAPI_BARRIER_NONE, + compute_barrier: UAPI_BARRIER_NONE, + } + ); + check_compute(payload, &bytes[compute_offset..]); + assert!(attachments.as_slice().is_empty()); + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(2)); +} + +#[test] +fn short_payloads_zero_extend_but_missing_bytes_are_rejected() { + // Every length includes cuts within each multi-byte field, for both kinds. + for (kind, size) in [(0, 256), (1, 64)] { + let supplied: Vec = (0..size).map(|i| (i % 254 + 1) as u8).collect(); + for length in 0..=size { + let raw = &supplied[..length]; + let bytes = frame(kind, raw); + let mut parser = UapiCommandParser::new(&bytes); + match parser.next_hardware().unwrap().unwrap() { + ParsedHardwareCommand::Render { payload, .. } => check_render(payload, raw), + ParsedHardwareCommand::Compute { payload, .. } => check_compute(payload, raw), + } + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(1)); + for cut in 1..bytes.len() { + assert!(parse(&bytes[..cut]).is_err()); + } + } + } + assert_eq!(parse(&[]), Err(UapiTranslateError::HardwareCommandCount)); +} + +#[test] +fn named_optional_timestamp_copies_are_refused() { + for (kind, name) in [ + (0, "cmd_render.ts_frag_end_copies"), + (1, "cmd_compute.ts_end_copies"), + ] { + let (offset, size) = layout()[name]; + let mut raw = vec![0; offset + size]; + assert_eq!(parse(&frame(kind, &raw)), Ok(1)); + for i in offset..raw.len() { + raw[i] = 1; + assert_eq!( + parse(&frame(kind, &raw)), + Err(UapiTranslateError::PayloadTail), + "{name} byte {i}" + ); + raw[i] = 0; + } + } +} + +#[test] +fn unsupported_nonzero_extensions_never_disappear() { + for (kind, size) in [(0, UAPI_RENDER_SIZE), (1, UAPI_COMPUTE_SIZE)] { + for extra in [1, 8, 64, 128] { + let mut payload = vec![0; size + extra]; + assert_eq!(parse(&frame(kind, &payload)), Ok(1)); + for offset in size..payload.len() { + payload[offset] = 1; + assert_eq!( + parse(&frame(kind, &payload)), + Err(UapiTranslateError::PayloadTail) + ); + payload[offset] = 0; + } + } + } + assert_eq!( + parse(&frame(0xffff, &[])), + Err(UapiTranslateError::CommandType) + ); +} + +#[test] +fn attachment_shapes_counts_and_reserved_fields_are_checked() { + for kind in 2..=4 { + for size in 1..UAPI_ATTACHMENT_SIZE { + assert_eq!( + parse(&frame(kind, &vec![0; size])), + Err(UapiTranslateError::AttachmentPayloadSize) + ); + } + let mut bytes = frame(kind, &vec![0; UAPI_ATTACHMENT_SIZE * UAPI_MAX_ATTACHMENTS]); + bytes.extend(frame(1, &[])); + assert_eq!(parse(&bytes), Ok(1)); + assert_eq!( + parse(&frame( + kind, + &vec![0; UAPI_ATTACHMENT_SIZE * (UAPI_MAX_ATTACHMENTS + 1)] + )), + Err(UapiTranslateError::AttachmentCount) + ); + for offset in 16..24 { + let mut payload = [0; UAPI_ATTACHMENT_SIZE]; + payload[offset] = 1; + assert_eq!( + parse(&frame(kind, &payload)), + Err(UapiTranslateError::AttachmentFields) + ); + } + let mut bytes = frame(kind, &[]); + bytes[4..6].copy_from_slice(&0u16.to_le_bytes()); + assert_eq!(parse(&bytes), Err(UapiTranslateError::AttachmentBarrier)); + } +} + +#[test] +fn attachment_lists_reach_their_commands() { + let entry = |pointer: u64, size: u64| { + let mut raw = [0u8; UAPI_ATTACHMENT_SIZE]; + raw[..8].copy_from_slice(&pointer.to_le_bytes()); + raw[8..16].copy_from_slice(&size.to_le_bytes()); + raw + }; + let vertex = entry(0x1_2345_6000, 0x1_0000_0040).to_vec(); + let fragment = [ + entry(0x2_0000_8000, 0x80), + entry(0x3_0000_c000, 0x2_0000_0100), + ] + .concat(); + let compute = entry(0x4_0000_1000, 0x5_0000_0010).to_vec(); + let check = |list: UapiAttachmentList, raw: &[u8]| { + assert_eq!(list.as_slice().len(), raw.len() / UAPI_ATTACHMENT_SIZE); + for (attachment, raw) in list.as_slice().iter().zip(raw.chunks(UAPI_ATTACHMENT_SIZE)) { + field(raw, "attachment.pointer", 0, 8, attachment.address); + field(raw, "attachment.size", 8, 8, attachment.size); + } + }; + + // Lists persist until replaced; an empty list clears only its own kind. + let mut bytes = frame(2, &vertex); + bytes.extend(frame(3, &fragment)); + bytes.extend(frame(4, &compute)); + bytes.extend(frame(0, &[])); + bytes.extend(frame(1, &[])); + bytes.extend(frame(3, &[])); + bytes.extend(frame(0, &[])); + let mut parser = UapiCommandParser::new(&bytes); + for expected_fragment in [&fragment[..], &[]] { + let Some(ParsedHardwareCommand::Render { + vertex_attachments, + fragment_attachments, + .. + }) = parser.next_hardware().unwrap() + else { + panic!("render missing") + }; + check(vertex_attachments, &vertex); + check(fragment_attachments, expected_fragment); + if !expected_fragment.is_empty() { + let Some(ParsedHardwareCommand::Compute { attachments, .. }) = + parser.next_hardware().unwrap() + else { + panic!("compute missing") + }; + check(attachments, &compute); + } + } + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(3)); +} + +#[test] +fn dependencies_count_hardware_commands_and_bound_the_submit() { + for field in [4, 6] { + let mut bytes = frame(1, &[]); + bytes[field..field + 2].copy_from_slice(&1u16.to_le_bytes()); + assert_eq!(parse(&bytes), Err(UapiTranslateError::BarrierFuture)); + } + let mut bytes = frame(2, &[]); + bytes.extend(frame(0, &[])); + let mut compute = frame(1, &[]); + compute[4..6].copy_from_slice(&1u16.to_le_bytes()); + bytes.extend(compute); + assert_eq!(parse(&bytes), Ok(2)); + let command = frame(1, &[]); + assert_eq!( + parse(&command.repeat(UAPI_MAX_HARDWARE_COMMANDS as usize)), + Ok(UAPI_MAX_HARDWARE_COMMANDS) + ); + assert_eq!( + parse(&command.repeat(UAPI_MAX_HARDWARE_COMMANDS as usize + 1)), + Err(UapiTranslateError::HardwareCommandCount) + ); +} + +struct Region { + start: u64, + end: u64, + writable: bool, +} +impl GpuAddressSpace for Region { + fn covers(&self, address: u64, size: u64, access: GpuAccess) -> bool { + let Some(end) = address.checked_add(size) else { + return false; + }; + address >= self.start && end <= self.end && (access == GpuAccess::Read || self.writable) + } +} + +#[test] +fn attachment_validation_rejects_readonly_zero_and_wrapped_ranges() { + let mut list = UapiAttachmentList::EMPTY; + list.count = 1; + let mut region = Region { + start: 0x4000, + end: 0x8000, + writable: false, + }; + list.entries[0] = UapiAttachment { + address: 0x4000, + size: 0x4000, + }; + assert_eq!( + validate_attachments(®ion, &list), + Err(UapiTranslateError::AddressRange) + ); + region.writable = true; + assert_eq!(validate_attachments(®ion, &list), Ok(())); + for (address, size) in [(0x4000, 0), (0x4000, 0x4001), (u64::MAX, 2)] { + list.entries[0] = UapiAttachment { address, size }; + assert_eq!( + validate_attachments(®ion, &list), + Err(UapiTranslateError::AddressRange) + ); + } +} + +#[test] +fn usc_windows_reject_misalignment_overflow_and_unmapped_programs() { + let queue = QueueUscWindow { + base: USC_WINDOW_SIZE, + user_start: 0x4000, + user_end: 2 * USC_WINDOW_SIZE, + }; + assert_eq!(queue.validate(), Ok(())); + assert_eq!( + QueueUscWindow { + base: queue.base + 1, + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscAlignment) + ); + assert_eq!( + QueueUscWindow { + base: u64::MAX & !(USC_WINDOW_SIZE - 1), + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscRange) + ); + assert_eq!( + QueueUscWindow { + user_end: queue.user_end - 1, + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscRange) + ); + let region = Region { + start: queue.base + 8, + end: queue.base + 12, + writable: false, + }; + assert_eq!(queue.full_program(®ion, 9), Ok(queue.base + 8)); + assert_eq!( + queue.full_program(®ion, 0), + Err(UapiTranslateError::ProgramOffset) + ); + assert_eq!( + queue.full_program(®ion, 16), + Err(UapiTranslateError::AddressRange) + ); +} + +#[test] +fn unsupported_firmware_cannot_reach_the_existing_profile() { + let identity = (1, 1, 0x07022000, [0xff, 0, 0]); + assert!(admission::validate_identity( + &[14, 8, 3], + identity.0, + identity.1, + identity.2, + identity.3 + ) + .is_some()); + for version in [&[26, 6, 2][..], &[14, 8][..], &[14, 8, 3, 0][..], &[][..]] { + assert!(admission::validate_identity( + version, identity.0, identity.1, identity.2, identity.3 + ) + .is_none()); + } +} + +#[test] +fn firmware_profile_agrees_with_the_actual_c_header() { + assert_eq!( + layout()["profile.params_required"].0 as u64, + m3_profile::PARAMS_REQUIRED + ); + assert_eq!(layout()["profile.vm_create_size"].0, 24); + assert_eq!( + layout()["profile.hal200"].0 as u32, + m3_profile::Profile::Extended.hal() + ); + assert_eq!(layout()["profile.features"].0, 0x0a); +} diff --git a/tools/asahi/test-m3-mailbox.py b/tools/asahi/test-m3-mailbox.py index 2320d17f10f6f4..167b0566b2e5dd 100644 --- a/tools/asahi/test-m3-mailbox.py +++ b/tools/asahi/test-m3-mailbox.py @@ -77,6 +77,17 @@ def vec(values): assert!(!ok(&compat,®,&names,&irqs,1)); let mut v=names.clone(); v[0]^=1; assert!(!ok(&compat,®,&v,&irqs,cells)); for v in [b"apple,asc-mailbox-v4\\0".to_vec(), b"apple,t8122-agx-asc-mailbox\\0unknown\\0".to_vec(), b"apple,t6031-agx-asc-mailbox\\0".to_vec()] { assert!(!ok(&v,®,&names,&irqs,cells)); } + // Compatible lists are ordered: reversing either supported pair must fail. + for pair in soc.mailbox_compatibles { + let parts: Vec<_> = pair.split(|byte| *byte == 0).filter(|part| !part.is_empty()).collect(); + if parts.len() == 2 { + let mut reversed = parts[1].to_vec(); + reversed.push(0); + reversed.extend_from_slice(parts[0]); + reversed.push(0); + assert!(!ok(&reversed,®,&names,&irqs,cells)); + } + } for v in soc.mailbox_compatibles { assert!(ok(&v.to_vec(),®,&names,&irqs,cells)); } } fn main() { diff --git a/tools/asahi/test-m3-uapi.py b/tools/asahi/test-m3-uapi.py new file mode 100755 index 00000000000000..a63249e18869d4 --- /dev/null +++ b/tools/asahi/test-m3-uapi.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile public C producers and run the production Rust parser offline.""" +import argparse +import hashlib +import os +from pathlib import Path +import subprocess +import sys +import tempfile + + +def run(command, **kwargs): + subprocess.run(command, check=True, **kwargs) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mesa", type=Path, help="matched public Mesa source") + args = parser.parse_args() + root = Path(__file__).resolve().parents[2] + if sys.byteorder != "little": + raise SystemExit("C fixtures currently require a little-endian host") + cc = os.environ.get("CC", "cc") + rustc = os.environ.get("RUSTC", "rustc") + fixture_source = root / "tools/asahi/m3-uapi-fixture.c" + with tempfile.TemporaryDirectory(prefix="asahi-uapi-") as temporary: + work = Path(temporary) + headers = work / "headers" + run(["make", "-s", "-C", str(root), "O=" + str(work / "headers-build"), + "ARCH=arm64", "headers_install", "INSTALL_HDR_PATH=" + str(headers)]) + sources = [(headers / "include", "drm/asahi_drm.h")] + if args.mesa: + sources.append((args.mesa.resolve() / "include", "drm-uapi/asahi_drm.h")) + fixtures = [] + layouts = [] + prefixes = [] + for index, (includes, header) in enumerate(sources): + producer = work / ("producer-" + str(index)) + run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", + "-I" + str(includes), '-DASAHI_HEADER="' + header + '"', + str(fixture_source), "-o", str(producer)]) + prefix = work / ("prefix-" + str(index)) + with prefix.open("wb") as output: + run([str(producer)], stdout=output) + prefixes.append(prefix) + layout = work / ("layout-" + str(index)) + with layout.open("wb") as output: + run([str(producer), "--layout"], stdout=output) + layouts.append(layout) + fixture = work / ("fixture-" + str(index)) + with fixture.open("wb") as output: + run([str(producer), "--full"], stdout=output) + fixtures.append(fixture) + reference = prefixes[0].read_bytes() + if any(prefix.read_bytes() != reference for prefix in prefixes[1:]): + raise SystemExit("kernel/Mesa canonical fixture mismatch") + print("canonical fixture sha256=" + hashlib.sha256(reference).hexdigest(), flush=True) + for optimized in (False, True): + binary = work / ("tests-optimized" if optimized else "tests-debug") + command = [rustc, "--edition=2021", "--test", + str(root / "tools/asahi/m3-uapi-tests.rs"), "-o", str(binary)] + if optimized: + command.append("-O") + run(command) + for index, fixture in enumerate(prefixes + fixtures): + env = dict(os.environ, ASAHI_UAPI_FIXTURE=str(fixture), + ASAHI_UAPI_LAYOUT=str(layouts[index % len(layouts)])) + run([str(binary)], env=env) + + +if __name__ == "__main__": + main() diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index 071fd7ad35fd07..4d2fbc3c53dafb 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2298,6 +2298,34 @@ "python3", "tools/asahi/test-m3-mailbox.py" ] + }, + { + "name": "tools/asahi/m3-firmware-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-firmware-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/test-m3-uapi.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-uapi.py" + ], + "rust": "1.85.0" } ], "helpers": { @@ -2305,7 +2333,8 @@ "tools/testing/selftests/drivers/gpu/asahi-g15/uat-owner.c": "compiled and executed by the registered asahi-g15/run.py actual reservation-owner fixture", "tools/testing/selftests/drivers/gpu/asahi-g15/pmp-bridge.c": "compiled and executed by the registered asahi-g15/run.py maintained PMP fixture", "tools/asahi/t6030-pmp-graph-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", - "tools/asahi/t6030-display-admission-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite" + "tools/asahi/t6030-display-admission-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", + "tools/asahi/m3-uapi-tests.rs": "compiled and executed by the registered tools/asahi/test-m3-uapi.py paired-header suite" }, "excluded": { "tools/aurora-sep/tests/coordinator/runtime-check.sh": "Needs /dev/sep-bio, a running kernel booted with trusted.source=applesep, the hw_random sysfs interface and fprintd. Coordinator payload for an approved device.", From 0667ef5f925fb7cb80a108e6ebbfaa5c0a4fe7fa Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:38:59 -0400 Subject: [PATCH 05/15] drm/asahi: construct and publish extended T8122 initialization data Build extended initialization owners, controller images and channel records with checked allocation spans. Publish the bounded loader table through existing M3 ownership. Keep extended runtime admission closed. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/m3/m3_channel_format.rs | 182 ++++++++ drivers/gpu/drm/asahi/m3/m3_compute_layout.rs | 171 ++++++-- drivers/gpu/drm/asahi/m3/m3_config.rs | 179 +++++++- drivers/gpu/drm/asahi/m3/m3_firmware.rs | 59 +++ drivers/gpu/drm/asahi/m3/m3_init_26.rs | 22 +- drivers/gpu/drm/asahi/m3/m3_mmio_26.rs | 33 +- drivers/gpu/drm/asahi/m3/m3_power_26.rs | 414 ++++++++++++++++++ drivers/gpu/drm/asahi/m3/mod.rs | 5 + tools/asahi/m3-runtime-tests.rs | 12 + tools/asahi/test-m3-power.py | 173 ++++++++ tools/aurora-ci/host-tests.json | 9 + 11 files changed, 1223 insertions(+), 36 deletions(-) create mode 100644 drivers/gpu/drm/asahi/m3/m3_channel_format.rs create mode 100644 drivers/gpu/drm/asahi/m3/m3_power_26.rs create mode 100644 tools/asahi/test-m3-power.py diff --git a/drivers/gpu/drm/asahi/m3/m3_channel_format.rs b/drivers/gpu/drm/asahi/m3/m3_channel_format.rs new file mode 100644 index 00000000000000..45eebda112b738 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_channel_format.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Bounded firmware channel records. Publication and retirement belong to Config. + +use super::m3_init_layout::Error; + +pub(crate) const CONTROL_BYTES: usize = 64; +pub(crate) const RECEIVE_BYTES: usize = 72; +pub(crate) const SLOTS: u32 = 256; + +#[derive(Clone, Copy, Debug)] +pub(crate) enum Action { + Initialize, + Register { + pipe: u32, + engine: u32, + }, + Idle { + enabled: bool, + }, + Release { + group: u8, + index: u8, + weight: u8, + address: u64, + }, +} + +impl Action { + /// The first two actions precede the first generation-bearing publication. + pub(crate) fn generation(self, published: u32, consumed: u32) -> Result { + match self { + Self::Initialize | Self::Register { .. } => { + if published != 0 || consumed != 0 { + return Err(Error::Address); + } + Ok(0) + } + _ => published.max(consumed).checked_add(1).ok_or(Error::Address), + } + } + + pub(crate) fn encode(self, output: &mut [u8]) -> Result<(), Error> { + let mut words = [0u32; 3]; + let mut release = None; + match self { + Self::Initialize => words[0] = 0x16, + Self::Register { pipe, engine } => { + if pipe >= 3 || !(1..=2).contains(&engine) { + return Err(Error::Address); + } + words = [0x1b, pipe, engine]; + } + Self::Idle { enabled } => words = [0x0a, enabled.into(), 0], + Self::Release { + group, + index, + weight, + address, + } => { + if index == 255 + || weight == 255 + || address < 0xffff_fc00_0000_0000 + || address & 7 != 0 + { + return Err(Error::Address); + } + words[0] = 0x14; + words[2] = u32::from_le_bytes([group, index, weight, 4]); + release = Some(address); + } + } + let bytes = output.get_mut(..CONTROL_BYTES).ok_or(Error::Size)?; + bytes.fill(0); + for (index, word) in words.iter().enumerate() { + bytes[index * 4..index * 4 + 4].copy_from_slice(&word.to_le_bytes()); + } + if let Some(address) = release { + bytes[12..20].copy_from_slice(&address.to_le_bytes()); + } + Ok(()) + } +} + +/// An empty ring is required before publishing a synchronous control transaction. +/// Both consumers must retire it before the next control action may be published. +pub(crate) fn idle_cursor(cursors: [u32; 3]) -> Result { + if cursors.iter().any(|value| *value >= SLOTS) { + return Err(Error::Bounds); + } + if cursors[0] != cursors[2] || cursors[1] != cursors[2] { + return Err(Error::Address); + } + Ok(cursors[2]) +} + +pub(crate) fn payload_offset(thread: usize, slot: u32) -> Result { + if thread >= 9 { + return Err(Error::Bounds); + } + Ok((thread * 256 + (slot & 255) as usize) * 0xd8) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initialization_records_have_only_their_defined_words() { + for action in [Action::Initialize, Action::Register { pipe: 2, engine: 1 }] { + let mut output = [0xa5; CONTROL_BYTES + 8]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[12..CONTROL_BYTES], &[0; CONTROL_BYTES - 12]); + assert_eq!(&output[CONTROL_BYTES..], &[0xa5; 8]); + assert_eq!(action.generation(0, 0), Ok(0)); + assert!(action.generation(1, 0).is_err()); + } + } + + #[test] + fn invalid_actions_preserve_the_entire_output() { + for action in [ + Action::Register { pipe: 3, engine: 1 }, + Action::Register { pipe: 0, engine: 0 }, + Action::Release { + group: 0, + index: 255, + weight: 1, + address: 0xffff_fc21_0000_0000, + }, + Action::Release { + group: 0, + index: 0, + weight: 1, + address: 8, + }, + ] { + let mut output = [0xa5; CONTROL_BYTES]; + assert!(action.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; CONTROL_BYTES]); + } + } + + #[test] + fn generations_follow_consumption_and_never_wrap() { + let action = Action::Idle { enabled: true }; + assert_eq!(action.generation(3, 5), Ok(6)); + assert_eq!(action.generation(5, 3), Ok(6)); + assert!(action.generation(u32::MAX, 0).is_err()); + assert!(action.generation(0, u32::MAX).is_err()); + let mut output = [0; CONTROL_BYTES]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[..8], &[10, 0, 0, 0, 1, 0, 0, 0]); + } + + #[test] + fn both_consumers_and_all_indices_are_checked() { + assert_eq!(idle_cursor([255, 255, 255]), Ok(255)); + for cursors in [[0, 1, 1], [1, 0, 1], [256, 256, 256]] { + assert!(idle_cursor(cursors).is_err()); + } + assert_eq!(payload_offset(8, 0x101), Ok((8 * 256 + 1) * 0xd8)); + assert!(payload_offset(9, 0).is_err()); + } + + #[test] + fn release_uses_the_packed_owned_pointer() { + let action = Action::Release { + group: 1, + index: 2, + weight: 3, + address: 0xffff_fc21_0400_0000, + }; + let mut output = [0; CONTROL_BYTES]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[8..12], &[1, 2, 3, 4]); + assert_eq!( + u64::from_le_bytes(output[12..20].try_into().unwrap()), + 0xffff_fc21_0400_0000 + ); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs index 74bed858b71e26..64c8cb005c4a1a 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT use super::{m3_pool_layout::GpuRegion, m3_queue_layout::{Error, FirmwareVa}}; pub(crate) const SIZE: usize = 0x893; +pub(crate) const EXTENDED_SIZE: usize = 0x8b0; pub(crate) const REGISTERS: usize = 0x20; pub(crate) const REGISTER_COUNT: usize = 22; /// G15G takes the same list without its last two registers (0xa599, 0xd411). @@ -64,12 +65,22 @@ impl Command { /// Validate before any output mutation. Encoding operates on ordinary host /// bytes; the caller copies them into the owned unpublished command buffer. pub(crate) fn encode(&self, out: &mut [u8]) -> Result<(), Error> { - if out.len() < SIZE { return Err(Error::Size); } + self.encode_for(super::m3_profile::Profile::Legacy, 0, 0, out) + } + + /// Firmware envelopes vary independently of the GPU's CDM stream format. + pub(crate) fn encode_for(&self, profile: super::m3_profile::Profile, + asid_generation: u8, event_slot: u8, out: &mut [u8]) -> Result<(), Error> { + let extended = profile == super::m3_profile::Profile::Extended; + let size = if extended { EXTENDED_SIZE } else { SIZE }; + if out.len() < size { return Err(Error::Size); } + if extended && (self.register_set != RegisterSet::G15G || event_slot >= 4 + || (self.context != 0 && asid_generation == 0)) { return Err(Error::Address); } if self.context >= 64 || self.cdm_last.get() < self.cdm.get() { return Err(Error::Address); } if self.preemption.size() < 0x14a0 || self.microsequence_size == 0 || self.microsequence_size > 4096 { return Err(Error::Size); } - self.gpu_alias.add((SIZE - 3) as u64)?; // inclusive final aligned dword + self.gpu_alias.add(((size - 1) & !3) as u64)?; // inclusive final aligned dword let registers = self.gpu_alias.add(REGISTERS as u64)?; let preemption = self.preemption.base(); let values = [ @@ -82,45 +93,143 @@ impl Command { (0x1a0b9, 0), (0x1a0c1, 0), (0x101d1, 0), (0xd479, 0), (0x1a0e9, 8), (0x107a1, 0xff0000), (0xa599, 0x13200400020), (0xd411, 0x200000001), ]; - let bytes = &mut out[..SIZE];bytes.fill(0); + let bytes = &mut out[..size];bytes.fill(0); word(bytes, 0, 3); // compute opcode - pointer(bytes, COUNTER, u64::from(self.counter)); - word(bytes, CONTEXT, self.context); - pointer(bytes, 0x14, self.notifier.get()); - let count = match self.register_set { + if extended { + word(bytes, 0xc, self.context); + pointer(bytes, 0x10, self.notifier.get()); + } else { + pointer(bytes, COUNTER, u64::from(self.counter)); + word(bytes, CONTEXT, self.context); + pointer(bytes, 0x14, self.notifier.get()); + } + let legacy_count = match self.register_set { RegisterSet::G15S => REGISTER_COUNT, RegisterSet::G15G => G15G_REGISTER_COUNT, }; - for (n, (register, value)) in values.into_iter().take(count).enumerate() { - word(bytes, REGISTERS+n*REGISTER_STRIDE, register); - pointer(bytes, REGISTERS+n*REGISTER_STRIDE+4, value); + let mut count = 0; + let mut emit = |register, value| { + word(bytes, REGISTERS + count * REGISTER_STRIDE, register); + pointer(bytes, REGISTERS + count * REGISTER_STRIDE + 4, value); + count += 1; + }; + let limit = if extended { REGISTER_COUNT } else { legacy_count }; + for (register, value) in values.into_iter().take(limit) { + if extended && register == 0x12091 { continue; } + emit(register, value); + } + if extended { + emit(0x1a540, 1); + emit(0x014a9, 1); + emit(0x0a351, 1); } pointer(bytes, 0x720, registers.get()); bytes[0x728..0x72a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x72a..0x72c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); pointer(bytes, 0x760, self.microsequence.get()); word(bytes, 0x768, self.microsequence_size); - // ComputeInfo2 mirrors the preemption buffer, inclusive CDM end and - // qualified control word from the register list. - pointer(bytes, 0x76c+0x28, preemption); - pointer(bytes, 0x76c+0x30, self.cdm_last.get()); - pointer(bytes, 0x76c+0x58, QUALIFIED_COMPUTE_CONTROL); - word(bytes, 0x7cc+0x14, u32::MAX); // EncoderParams.unk_mask - pointer(bytes, META, self.stamp.get()); - pointer(bytes, META+8, self.fw_stamp.get()); - word(bytes, STAMP_VALUE, self.stamp_value); - word(bytes, META+0x14, 2); // qualified compute event slot - word(bytes, FLUSH_STAMPS, u32::from(self.flush_stamps)); - word(bytes, META+0x20, 0xc10000); // producer UUID - word(bytes, QUEUE_COUNT, self.counter); - pointer(bytes, TAIL+8, self.timestamps[0].get()); - pointer(bytes, TAIL+16, self.timestamps[1].get()); - pointer(bytes, TAIL+0x2e, self.pool.get()); - bytes[TAIL+0x36] = 1; // SharedCommandTail.unk_846 - pointer(bytes, TAIL+0x37, 0xad0000); - pointer(bytes, TAIL+0x3f, 0x1600000); - pointer(bytes, TAIL+0x47, self.tail_scratch.get()); - bytes[TAIL+0x4f] = 1; // SharedCommandTail.unk_85f + let info = if extended { 0x770 } else { 0x76c }; + pointer(bytes, info + 0x28, preemption); + pointer(bytes, info + 0x30, self.cdm_last.get()); + pointer(bytes, info + 0x58, QUALIFIED_COMPUTE_CONTROL); + word(bytes, if extended { 0x7e8 } else { 0x7e0 }, u32::MAX); + let meta = if extended { 0x800 } else { META }; + pointer(bytes, meta, self.stamp.get()); + pointer(bytes, meta + 8, self.fw_stamp.get()); + word(bytes, meta + 0x10, self.stamp_value); + word(bytes, meta + 0x14, 2); + if extended { word(bytes, meta + 0x18, event_slot.into()); } + word(bytes, meta + 0x1c, u32::from(self.flush_stamps)); + word(bytes, meta + 0x20, 0xc10000); + word(bytes, meta + 0x24, self.counter); + let tail = if extended { 0x82c } else { TAIL }; + pointer(bytes, tail + 8, self.timestamps[0].get()); + pointer(bytes, tail + 16, self.timestamps[1].get()); + pointer(bytes, tail + if extended { 0x3e } else { 0x2e }, self.pool.get()); + bytes[tail + if extended { 0x46 } else { 0x36 }] = 1; + pointer(bytes, tail + if extended { 0x47 } else { 0x37 }, + if extended { 0x1588000 } else { 0xad0000 }); + pointer(bytes, tail + if extended { 0x4f } else { 0x3f }, + if extended { 0xef8000 } else { 0x1600000 }); + pointer(bytes, tail + if extended { 0x57 } else { 0x47 }, self.tail_scratch.get()); + if extended { bytes[0x88b] = asid_generation; } + else { bytes[TAIL + 0x4f] = 1; } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use super::super::m3_profile::Profile; + + fn fixture() -> Command { + let firmware = |n: u64| FirmwareVa::new(0xffff_fc21_8000_0000 + n * 0x4000).unwrap(); + Command { + context: 3, counter: 7, notifier: firmware(0), + preemption: GpuRegion::new(0x5000_0000, 0x4000).unwrap(), + cdm: GpuVa::new(0x6000_0000).unwrap(), + cdm_last: GpuVa::new(0x6000_0ffc).unwrap(), + gpu_alias: GpuVa::new(0x7000_0000).unwrap(), + microsequence: firmware(1), microsequence_size: 0x300, + stamp: firmware(2), fw_stamp: firmware(3), stamp_value: 0x700, + timestamps: [firmware(4), firmware(5)], pool: firmware(6), + tail_scratch: firmware(7), flush_stamps: true, + register_set: RegisterSet::G15G, + } + } + fn read32(b: &[u8], o: usize) -> u32 { + u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) + } + fn read64(b: &[u8], o: usize) -> u64 { + u64::from_le_bytes(b[o..o + 8].try_into().unwrap()) + } + + #[test] + fn extended_envelope_preserves_stream_and_native_visibility_boundary() { + let c = fixture(); + let mut b = [0xa5; EXTENDED_SIZE + 16]; + c.encode_for(Profile::Extended, 9, 2, &mut b).unwrap(); + assert_eq!(read32(&b, 0xc), c.context); + assert_eq!(read64(&b, 0x10), c.notifier.get()); + assert_eq!(read32(&b, 0x728) & 0xffff, 24); + let count = (read32(&b, 0x728) & 0xffff) as usize; + let cdm = (0..count).find(|n| read32(&b, 0x20 + n * 12) == 0x1a420).unwrap(); + assert_eq!(read64(&b, 0x24 + cdm * 12), c.cdm.get()); + assert_eq!(read64(&b, 0x7a0), c.cdm_last.get()); + assert_eq!(read64(&b, 0x800), c.stamp.get()); + assert_eq!(read64(&b, 0x808), c.fw_stamp.get()); + assert_eq!(read32(&b, 0x818), 2); + assert_eq!(read32(&b, 0x81c), 1); + assert_eq!(read64(&b, 0x834), c.timestamps[0].get()); + assert_eq!(read64(&b, 0x86a), c.pool.get()); + assert_eq!(read64(&b, 0x883), c.tail_scratch.get()); + assert_eq!(b[0x88b], 9); + assert_eq!(&b[EXTENDED_SIZE..], &[0xa5; 16]); + } + + #[test] + fn extended_validation_never_mutates_a_rejected_envelope() { + for (generation, slot) in [(0, 0), (1, 4), (1, 255)] { + let mut b = [0xa5; EXTENDED_SIZE]; + assert!(fixture().encode_for(Profile::Extended, generation, slot, &mut b).is_err()); + assert_eq!(b, [0xa5; EXTENDED_SIZE]); + } + let mut b = [0xa5; EXTENDED_SIZE - 1]; + assert!(fixture().encode_for(Profile::Extended, 1, 0, &mut b).is_err()); + assert_eq!(b, [0xa5; EXTENDED_SIZE - 1]); + } + + #[test] + fn legacy_wrapper_keeps_legacy_geometry_and_register_order() { + let c = fixture(); + let mut b = [0xa5; EXTENDED_SIZE]; + c.encode(&mut b).unwrap(); + assert_eq!(read32(&b, CONTEXT), c.context); + assert_eq!(read64(&b, 0x14), c.notifier.get()); + assert_eq!(read32(&b, 0x728) & 0xffff, 20); + assert_eq!(read32(&b, REGISTERS + 8 * REGISTER_STRIDE), 0x12091); + assert_eq!(read64(&b, META), c.stamp.get()); + assert_eq!(&b[SIZE..], &[0xa5; EXTENDED_SIZE - SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_config.rs b/drivers/gpu/drm/asahi/m3/m3_config.rs index c4a44c4b4f3b0e..67f4f963637f23 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -49,6 +49,8 @@ impl Records { pub(crate) struct Config { objects: KVec, + profile: super::m3_profile::Profile, + control_generation: u32, _iomaps: KVec, pub(crate) completed_events: u64, pstates: crate::m3::m3_adt_config::PstateWatch, @@ -140,7 +142,8 @@ impl Config { crate::m3::m3_adt_config::check_upload(dev.as_ref().as_ref(), firmware, contents.iomaps, &mut objects)?; agx_memory::publish(); let thermal = crate::m3::m3_thermal::Governor::new(dev.as_ref().as_ref(), &contents.pstates)?; - Ok(Self { objects, _iomaps: iomaps, completed_events: 0, + Ok(Self { objects, profile: firmware.profile(), control_generation: 0, + _iomaps: iomaps, completed_events: 0, pstates: crate::m3::m3_adt_config::PstateWatch::new(contents.pstates), thermal, request_top:contents.table.0,request_value:0,request_checked:false, request_check_at:None,request_writes:0,request_observed:0 }) @@ -261,6 +264,9 @@ impl Config { Ok(true) } pub(crate) fn ready(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + return Ok(self.objects[RUNTIME_FLAGS].read_u32(0x14)? == 1); + } // NewRegionB+4 is an input initialized to 1, not an acknowledgement. // These are firmware-written transitions present in compute-15's // pre-submit snapshot; both start at zero in the exported schema. @@ -343,10 +349,38 @@ impl Config { agx_memory::publish(); Ok(next) } + /// Publish an extended-format synchronous control. The caller rings the + /// doorbell after this method and waits for both retirement cursors. + pub(crate) fn control_extended(&mut self, action: super::m3_channel_format::Action) -> Result { + use super::m3_channel_format as format; + if self.profile != super::m3_profile::Profile::Extended { return Err(EINVAL); } + let state = storage::DEVICE_CONTROL * 2; + let cursors = [self.objects[state].read_u32(0)?, + self.objects[state].read_u32(0x10)?, self.objects[state].read_u32(0x20)?]; + let cursor = format::idle_cursor(cursors).map_err(|_| EBUSY)?; + let consumed = self.objects[CONTROL_REGION].read_u32(0xeae8)?; + let generation = action.generation(self.control_generation, consumed).map_err(|_| EOVERFLOW)?; + let mut record = [0; format::CONTROL_BYTES]; + action.encode(&mut record).map_err(|_| EINVAL)?; + self.objects[state + 1].write(cursor as usize * format::CONTROL_BYTES, &record)?; + if generation != 0 { + self.objects[GLOBALS].write(0xe41, &generation.to_le_bytes())?; + } + agx_memory::publish(); + let next = (cursor + 1) % format::SLOTS; + self.objects[state].u32(0x20, next)?; + agx_memory::publish(); + self.control_generation = generation; + Ok(next) + } + pub(crate) fn control_done(&mut self, next: u32) -> Result { Ok(self.objects[storage::DEVICE_CONTROL*2].read_u32(0)? == next && self.objects[storage::DEVICE_CONTROL*2].read_u32(0x10)? == next) } pub(crate) fn drain(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + return self.drain_extended(dev); + } // Event, trace, stats and six independent firmware log rings. for (state, ring, size, count, channels) in [(storage::EVENT*2,storage::EVENT*2+1,0x38,256,1), (storage::TRACE*2,storage::TRACE*2+1,0x38,512,1), @@ -392,4 +426,147 @@ impl Config { } Ok(()) } + fn drain_extended(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + use super::m3_channel_format as format; + // Snapshot each producer once. New arrivals remain for the next drain; + // a corrupt cursor is rejected before reading its ring allocation. + for (channel, capacity, threads) in [(storage::FIRMWARE_LOG, 256, 9), + (storage::EVENT, 256, 1), (storage::TRACE, 512, 1), (storage::STATS, 256, 1)] { + let state = channel * 2; + for thread in 0..threads { + let at = thread * 0x30; + let mut read = self.objects[state].read_u32(at)?; + let write = self.objects[state].read_u32(at + 0x20)?; + if read >= capacity || write >= capacity { return Err(EIO); } + let before = read; + while read != write { + let offset = (thread * capacity as usize + read as usize) * format::RECEIVE_BYTES; + let mut record = [0; format::RECEIVE_BYTES]; + self.objects[state + 1].read(offset, &mut record)?; + let kind = u32::from_le_bytes(record[..4].try_into().map_err(|_| EIO)?); + if channel == storage::FIRMWARE_LOG { + let slot = u32::from_le_bytes(record[8..12].try_into().map_err(|_| EIO)?); + let payload = format::payload_offset(thread, slot).map_err(|_| EIO)?; + if crate::debug::debug_enabled(crate::debug::DebugFlags::KTraceCh) { + let mut text = [0; 200]; + self.objects[FWLOG_PAYLOAD].read(payload + 0x10, &mut text)?; + let len = text.iter().position(|value| *value == 0).unwrap_or(text.len()); + dev_info!(dev.as_ref(), "M3 firmware log[{}]: {}\n", thread, + core::str::from_utf8(&text[..len]).unwrap_or("")); + } + } else if channel == storage::EVENT { + if kind != 1 { + dev_err!(dev.as_ref(), "M3 firmware event {}: {:02x?}\n", kind, record); + return Err(EIO); + } + let flags = u32::from_le_bytes(record[4..8].try_into().map_err(|_| EIO)?); + self.completed_events = self.completed_events.saturating_add((flags & 7).count_ones().into()); + } else if (channel == storage::TRACE && kind != 5) + || (channel == storage::STATS && kind == 0x16) { + dev_err!(dev.as_ref(), "M3 firmware channel {} record {}: {:02x?}\n", channel, kind, record); + return Err(EIO); + } + read = (read + 1) % capacity; + } + if read != before { self.objects[state].u32(at, read)?; } + } + } + agx_memory::publish(); + Ok(()) + } + +} + +/// All extended initialization owners, retained as one construction transaction. +/// Nothing is published to RTKit by this constructor. A failed allocation drops +/// its mappings before backing storage and leaves no root visible to firmware. +pub(crate) struct ExtendedStorage { + pub(crate) objects: KVec, + pub(crate) iomaps: KVec, + pub(crate) descriptors: KVec, + pub(crate) maximum_khz: u32, +} + +impl ExtendedStorage { + pub(crate) fn new( + dev: &driver::AsahiDrmDevice, + uat: &mmu::Uat, + pwr: &super::hw::PwrConfig, + sram_uv: &[u32], + bases: super::m3_mmio_26::RegisterBases, + ) -> Result { + use super::{m3_init_26 as plan, m3_mmio_26 as io}; + let mut objects = KVec::new(); + for index in 0..plan::COUNT { + let entry = plan::allocation(index).map_err(|_| EINVAL)?; + let alias = match index { + plan::GLOBAL_POOL => Some((uat.kernel_lower_vm(), 0x70_0000_0000)), + plan::GLOBAL_PARAMETERS => Some((uat.kernel_lower_vm(), 0x70_0000_8000)), + _ => None, + }; + let object = if plan::cached(index) { + Buffer::at_prot(dev, uat.kernel_vm(), alias, Some(entry.address), entry.size, + prot::PROT_FW_PRIV_RW, prot::PROT_GPU_SHARED_RW)? + } else { + Buffer::at(dev, uat.kernel_vm(), alias, Some(entry.address), entry.size, entry.gpu_shared)? + }; + objects.push(object, GFP_KERNEL)?; + } + let mut descriptors = KVec::new(); + descriptors.resize(io::SLOTS * io::RECORD_BYTES, 0, GFP_KERNEL)?; + let mut iomaps = KVec::new(); + for slot in 0..io::SLOTS { + let mut region = None; + if let Some(area) = io::area(slot, bases).map_err(|_| EINVAL)? { + let address = 0xffff_fc23_0000_0000 + slot as u64 * 0x10_0000; + let access = if area.writable() { prot::PROT_FW_MMIO_RW } else { prot::PROT_FW_MMIO_RO }; + for (physical, bytes, offset) in area.banks() { + if bytes == 0 { continue; } + let mapping = uat.kernel_vm().map_io_m3(address + offset as u64, + physical.try_into()?, bytes, access)?; + iomaps.push(mapping, GFP_KERNEL)?; + } + region = Some(init::Region::new(address, area.mapped_bytes()).map_err(|_| EINVAL)?); + } + io::encode(slot, bases, region, + &mut descriptors[slot * io::RECORD_BYTES..(slot + 1) * io::RECORD_BYTES]) + .map_err(|_| EINVAL)?; + } + // Runtime+0x18 references Fender SRAM, rather than anonymous DMA RAM. + // Retain its normal shared mapping independently of its MMIO view. + let scratch_physical = bases.gpu.checked_add(0xd6_0000).ok_or(EOVERFLOW)?; + let scratch = uat.kernel_vm().map_io_m3(0xffff_fc22_0000_0000, + scratch_physical.try_into()?, 0x8000, prot::PROT_FW_SHARED_RW)?; + let scratch_region = init::Region::new(scratch.iova(), 0x8000).map_err(|_| EINVAL)?; + iomaps.push(scratch, GFP_KERNEL)?; + let mut regions = [init::Region::new(0xffff_fc21_0000_0000, 1).map_err(|_| EINVAL)?; plan::COUNT]; + for (index, object) in objects.iter().enumerate() { + regions[index] = init::Region::new(object.va(), object.size()).map_err(|_| EINVAL)?; + } + let graph = plan::PointerGraph { owners: ®ions, scratch: scratch_region, + pool_alias: objects[plan::GLOBAL_POOL].gpu_va()?, + parameter_alias: objects[plan::GLOBAL_PARAMETERS].gpu_va()? }; + let mut root = [0; 0xc8]; + let mut runtime = [0; plan::RUNTIME_SIZE]; + let mut control = KVec::new(); + control.resize(plan::allocation(CONTROL_REGION).map_err(|_| EINVAL)?.size, 0, GFP_KERNEL)?; + graph.root(&mut root).map_err(|_| EINVAL)?; + graph.runtime(&mut runtime).map_err(|_| EINVAL)?; + graph.firmware_control(&mut control).map_err(|_| EINVAL)?; + let images = super::m3_power_26::PowerImages::new(pwr, sram_uv, + objects[plan::SENSOR_VALUE].va(), objects[plan::RETENTION_STATE].va(), + agx_memory::TIMESTAMP_RANGE.start, &descriptors)?; + objects[storage::ROOT].write(0, &root)?; + objects[RUNTIME_POINTERS].write(0, &runtime)?; + objects[CONTROL_REGION].write(0, &control)?; + objects[HARDWARE_DATA].write(0, &images.hardware)?; + objects[plan::PERFORMANCE_CONTROL].write(0, &images.performance)?; + objects[GLOBALS_POWER].write(0, &images.power)?; + objects[GLOBALS].write(0, &images.globals)?; + // Fragment scheduler entries start unassigned; these are input sentinels. + objects[UNKNOWN_C1].u32(0xc18, u32::MAX)?; + objects[UNKNOWN_C1].u32(0xc30, u32::MAX)?; + agx_memory::publish(); + Ok(Self { objects, iomaps, descriptors, maximum_khz: images.maximum_khz }) + } } diff --git a/drivers/gpu/drm/asahi/m3/m3_firmware.rs b/drivers/gpu/drm/asahi/m3/m3_firmware.rs index c9aa45cc028648..6bc87a33aaa052 100644 --- a/drivers/gpu/drm/asahi/m3/m3_firmware.rs +++ b/drivers/gpu/drm/asahi/m3/m3_firmware.rs @@ -255,6 +255,51 @@ impl Firmware { } } +/// Only the admitted extended image has this host-owned boot descriptor slot. +fn loader_span(profile: super::m3_profile::Profile, data_bytes: u64, input_bytes: usize) + -> Option> { + if profile != super::m3_profile::Profile::Extended || input_bytes != 53 * 40 + || data_bytes != T8122_LAYOUT_26.data_size { return None; } + let start: usize = 0x12_28e0; + let end = start.checked_add(input_bytes)?; + if end as u64 > data_bytes { return None; } + Some(start..end) +} + +#[cfg(not(test))] +impl Firmware { + /// Call after RTKit application startup and before exposing the InitData root. + /// The protocol grants the host this slot only during that boot interval. + pub(crate) fn publish_io_table( + &self, + pdev: &kernel::platform::Device>, + descriptors: &[u8], + ) -> kernel::error::Result { + use kernel::{c_str, io::mem::{Mem, MemFlag}, prelude::*}; + let data = self.resources.regions[5]; + let span = loader_span(self.profile(), data.size, descriptors.len()).ok_or(EINVAL)?; + let node = pdev.as_ref().of_node().ok_or(ENODEV)?; + let resource = agx_resources::reserved_resource(&node, c_str!("fw-data"))?; + if resource.start() != data.base || resource.size() != data.size { return Err(EINVAL); } + // SAFETY: This exact admitted no-map reservation remains firmware-owned. + // Only its bounded host boot slot is accessed, before root publication. + let mapping = unsafe { Mem::try_new(resource, MemFlag::WC.into()) }?; + if span.end > mapping.size() { return Err(EINVAL); } + // Validate the whole slot before changing any byte. Unexpected prior + // contents indicate a protocol/ownership mismatch, not a patchable image. + for offset in span.clone() { + // SAFETY: span is bounded by the retained mapping above. + if unsafe { mapping.ptr().add(offset).read_volatile() } != 0 { return Err(EBUSY); } + } + for (offset, byte) in span.zip(descriptors.iter().copied()) { + // SAFETY: same bounded host slot, exclusive under the boot protocol. + unsafe { mapping.ptr().add(offset).write_volatile(byte) }; + } + crate::m3::agx_memory::publish(); + Ok(()) + } +} + #[cfg(not(test))] pub(crate) fn identify_loaded( pdev: &kernel::platform::Device>, @@ -527,4 +572,18 @@ mod tests { assert!(!newer.accepts_runtime(KNOWN_IMAGES_T8122[0].uuid, &[0; 32])); } + #[test] + fn loader_slot_is_exact_bounded_and_profile_specific() { + use super::super::m3_profile::Profile; + let length = 53 * 40; + assert_eq!(loader_span(Profile::Extended, 0x12c000, length), Some(0x1228e0..0x123128)); + for size in [0, 0x123127, 0x12bfff, 0x12c001, u64::MAX] { + assert!(loader_span(Profile::Extended, size, length).is_none()); + } + for bytes in [0, length - 1, length + 1, usize::MAX] { + assert!(loader_span(Profile::Extended, 0x12c000, bytes).is_none()); + } + assert!(loader_span(Profile::Legacy, 0x12c000, length).is_none()); + } + } diff --git a/drivers/gpu/drm/asahi/m3/m3_init_26.rs b/drivers/gpu/drm/asahi/m3/m3_init_26.rs index f08f16200b0700..a86432b2bbe2b5 100644 --- a/drivers/gpu/drm/asahi/m3/m3_init_26.rs +++ b/drivers/gpu/drm/asahi/m3/m3_init_26.rs @@ -62,7 +62,7 @@ pub(crate) fn allocation(index: usize) -> Result { } pub(crate) fn cached(index: usize) -> bool { - matches!(index, 38 | 39 | 42 | 43 | 44 | PERFORMANCE_CONTROL) + matches!(index, 39 | PERFORMANCE_CONTROL) } pub(crate) struct PointerGraph<'a> { @@ -103,7 +103,10 @@ impl PointerGraph<'_> { } let scratch = self.scratch.at(0, 0x8000)?; let scratch_end = scratch.checked_add(0x8000).ok_or(Error::Address)?; - if scratch & 0x3fff != 0 || scratch_end > 0xffff_fc2f_fc00_0000 { + if scratch & 0x3fff != 0 + || scratch < 0xffff_fc20_0000_0000 + || scratch_end > 0xffff_fc2f_fc00_0000 + { return Err(Error::Address); } for (index, address) in addresses.iter().enumerate() { @@ -272,4 +275,19 @@ mod tests { assert!(graph(&owners).runtime(&mut output).is_err()); assert_eq!(output, [0xa5; RUNTIME_SIZE]); } + #[test] + fn only_atomic_controller_owners_are_firmware_cacheable() { + let selected: Vec<_> = (0..COUNT).filter(|index| cached(*index)).collect(); + assert_eq!(selected, vec![39, PERFORMANCE_CONTROL]); + } + + #[test] + fn scratch_requires_its_firmware_host_window() { + let owners = owners(0); + let mut value = graph(&owners); + value.scratch = Region::new(0xffff_fc10_0000_0000, 0x8000).unwrap(); + let mut output = [0xa5; RUNTIME_SIZE]; + assert_eq!(value.runtime(&mut output), Err(Error::Address)); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } } diff --git a/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs index e4008f239c4a0d..a17bc0ee399c34 100644 --- a/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs +++ b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs @@ -18,6 +18,10 @@ pub(crate) struct Area { } impl Area { + pub(crate) fn writable(self) -> bool { + self.writable + } + pub(crate) fn page_offset(self) -> usize { (self.physical & 0x3fff) as usize } @@ -78,6 +82,16 @@ pub(crate) fn area(slot: usize, bases: RegisterBases) -> Result, Er if physical == 0 || physical.checked_add(bytes as u64).ok_or(Error::Address)? > 1 << 42 { return Err(Error::Address); } + if split + && (physical & 0x3fff != 0 + || physical + .checked_add(0x200_0000) + .and_then(|v| v.checked_add((bytes / 2) as u64)) + .filter(|v| *v <= 1 << 42) + .is_none()) + { + return Err(Error::Address); + } Ok(Some(Area { physical, bytes, @@ -143,8 +157,7 @@ pub(crate) fn encode( mod tests { use super::*; - // Register identities already present in the public T8122 configuration; - // memory-controller bank geometry is shared with the public 25G83 encoder. + // Resource owners exercise subpage mappings and split memory-controller banks. const BASES: RegisterBases = RegisterBases { gpu: 0x2_9000_0000, interrupt: 0x2_d101_4000, @@ -262,4 +275,20 @@ mod tests { assert_eq!(output, [0xa5; RECORD_BYTES]); } } + #[test] + fn split_banks_require_page_alignment_and_both_fit_the_bus() { + for memory in [BASES.memory + 4, (1 << 42) - 0x100_0000] { + let mut bases = BASES; + bases.memory = memory; + let mut output = [0xa5; RECORD_BYTES]; + assert!(encode( + 3, + bases, + Some(Region::new(0xffff_fc21_0000_0000, 0x30000).unwrap()), + &mut output + ) + .is_err()); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + } } diff --git a/drivers/gpu/drm/asahi/m3/m3_power_26.rs b/drivers/gpu/drm/asahi/m3/m3_power_26.rs new file mode 100644 index 00000000000000..2619c24b4eb6a3 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_power_26.rs @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Build the extended M3 controller images from admitted device-tree inputs. +//! These are constructor inputs. Firmware owns subsequent controller outputs. + +use super::{hw, initdata::PStateTables}; +use crate::{f32, float::F32}; +use kernel::prelude::*; + +struct Image(KVec); +impl Image { + fn new(size: usize) -> Result { + let mut bytes = KVec::new(); + bytes.resize(size, 0, GFP_KERNEL)?; + Ok(Self(bytes)) + } + fn word(&mut self, offset: usize, value: u32) -> Result { + self.0 + .get_mut(offset..offset.checked_add(4).ok_or(EOVERFLOW)?) + .ok_or(ERANGE)? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) + } + fn pointer(&mut self, offset: usize, value: u64) -> Result { + self.0 + .get_mut(offset..offset.checked_add(8).ok_or(EOVERFLOW)?) + .ok_or(ERANGE)? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) + } + fn float(&mut self, offset: usize, value: F32) -> Result { + let bits = value.to_bits(); + if bits & 0x7f80_0000 == 0x7f80_0000 { + return Err(EINVAL); + } + self.word(offset, bits) + } + fn reciprocal(&mut self, negative: usize, positive: usize, periods: u32) -> Result { + if periods == 0 { + return Err(EINVAL); + } + let alpha = f32!(1.0) / F32::from(periods); + self.float(negative, f32!(1.0) - alpha)?; + self.float(positive, alpha) + } +} + +pub(crate) struct PowerImages { + pub(crate) hardware: KVec, + pub(crate) performance: KVec, + pub(crate) power: KVec, + pub(crate) globals: KVec, + pub(crate) maximum_khz: u32, +} + +impl PowerImages { + /// `sram_uv` follows the same OPP iteration order as PwrConfig::perf_states. + /// No reference-bin voltage or experimental OPP-power estimate is substituted. + pub(crate) fn new( + pwr: &hw::PwrConfig, + sram_uv: &[u32], + sensor: u64, + retention: u64, + timestamp_base: u64, + io_records: &[u8], + ) -> Result { + let table = PStateTables::new(pwr, true, None, false)?; + let second = table.secondary.ok_or(EINVAL)?; + if table.len != 9 + || table.max != 8 + || sram_uv.len() != pwr.perf_states.len() + || pwr.perf_base_pstate == 0 + || pwr.perf_base_pstate > 8 + || !(1..=1000).contains(&pwr.power_sample_period) + || pwr.pwr_min_duty_cycle > 100 + || pwr.avg_power_min_duty_cycle > 100 + || io_records.len() != super::m3_mmio_26::SLOTS * super::m3_mmio_26::RECORD_BYTES + || [sensor, retention, timestamp_base] + .iter() + .any(|value| *value < 0xffff_fc00_0000_0000 || value & 7 != 0) + { + return Err(EINVAL); + } + let top = &pwr.perf_states[table.primary[8] as usize]; + let base = &pwr.perf_states[table.primary[pwr.perf_base_pstate as usize] as usize]; + if top.freq_hz != 1_338_000_000 || base.freq_hz >= top.freq_hz { + return Err(EINVAL); + } + let period = pwr.power_sample_period; + let ppm_periods = pwr.ppm_filter_time_constant_ms / period; + let average_periods = pwr.avg_power_filter_tc_ms / period; + if [ + ppm_periods, + average_periods, + pwr.pwr_filter_time_constant, + pwr.perf_filter_time_constant, + pwr.perf_filter_time_constant2, + pwr.avg_power_target_filter_tc, + pwr.se_filter_time_constant, + pwr.se_filter_time_constant_1, + ] + .contains(&0) + { + return Err(EINVAL); + } + let clocks = |count: u32| { + count + .checked_mul(period) + .and_then(|v| v.checked_mul(24_000)) + .ok_or(EOVERFLOW) + }; + let dt = F32::from(period) / f32!(1000.0); + let ceiling = 800; + let base_units = pwr.perf_base_pstate * 100; + // The profile controller budget is independent of the loader's experimental + // OPP power model. It is not a demand floor or a replacement voltage table. + let budget = 22_044u32; + let mut main = Image::new(0x2658)?; + let mut performance = Image::new(0x6578)?; + let mut power = Image::new(0x670)?; + let mut globals = Image::new(0xe48)?; + + main.pointer(0x28, timestamp_base)?; + main.0[0x640..0xe88].copy_from_slice(io_records); + main.pointer(0xe88, retention)?; + main.word(0xe90, 0x8122)?; + main.word(0xe94, 2)?; + main.word(0xe9c, 4)?; + main.word(0xea0, 1)?; + main.word(0xea4, 1)?; + main.word(0xeb8, 1)?; + main.word(0xec0, 1)?; + main.word(0xec8, 1)?; + main.word(0xed0, 24_000)?; + main.word(0xed4, 1)?; + main.word(0xed8, period)?; + main.word(0xee0, 1)?; + main.word(0xee4, 1)?; + main.word(0xee8, 1)?; + main.word(0xf04, 31)?; + main.pointer(0xf24, 4)?; + main.word(0xf34, 1)?; + // Firmware completion is polled through its normal bounded status path. + main.word(0xf38, 0)?; + main.word(0xf4c, 49)?; + main.word(0xf6c, 1)?; + main.word(0xf88, 10)?; + main.word(0xf8c, 3)?; + main.word(0xfac, 1)?; + main.word(0xfb8, 22)?; + main.word(0xfbc, 5)?; + main.word(0xfc0, 10)?; + main.word(0xfc4, 8)?; + main.word(0x1cd8, 8)?; + for state in 0..9 { + let primary = table.primary[state] as usize; + let secondary = second[state] as usize; + let item = &pwr.perf_states[primary]; + let auxiliary = &pwr.perf_states[secondary]; + if item.volt_mv.len() != 1 + || auxiliary.volt_mv.len() != 1 + || !(100..=1100).contains(&item.volt_mv[0]) + || !(100..=1100).contains(&auxiliary.volt_mv[0]) + || !(100_000..=1_100_000).contains(&sram_uv[primary]) + || !(100_000..=1_100_000).contains(&sram_uv[secondary]) + || sram_uv[primary].div_ceil(1000) < item.volt_mv[0] + || sram_uv[secondary].div_ceil(1000) < auxiliary.volt_mv[0] + { + return Err(EINVAL); + } + main.word(0xfc8 + state * 4, item.freq_hz / 1_000_000)?; + main.word(0x1808 + state * 4, auxiliary.freq_hz / 1_000_000)?; + main.float(0x1848 + state * 4, f32!(1.02))?; + for lane in 0..16 { + main.word(0x1008 + state * 64 + lane * 4, item.volt_mv[0])?; + main.word( + 0x1408 + state * 64 + lane * 4, + sram_uv[primary].div_ceil(1000), + )?; + } + main.word(0x1cdc + state * 4, auxiliary.freq_hz / 1_000_000)?; + if state != 0 { + main.word(0x1d1c + state * 64, auxiliary.volt_mv[0])?; + } + main.word(0x211c + state * 64, sram_uv[secondary].div_ceil(1000))?; + if state != 0 { + // Profile indices for the admitted eight-state ladder. + let core_profile = if state <= 3 { state } else { 2 * state - 3 }; + let auxiliary_profile = if state <= 4 { state } else { 2 * state - 4 }; + main.word(0x19c8 + state * 4, core_profile as u32)?; + main.word(0x1a08 + state * 4, auxiliary_profile as u32)?; + let boost = item.freq_hz.saturating_sub(base.freq_hz) / 1_000_000; + main.word( + 0x1908 + state * 4, + 100 * boost / ((top.freq_hz - base.freq_hz) / 1_000_000), + )?; + } + } + // The native relative-power calibration of each running firmware state. + // Encode only these semantic percentages, not an object-image overlay. + for (state, percent) in [15, 25, 36, 46, 58, 75, 90, 100].into_iter().enumerate() { + main.word(0x18c8 + (state + 1) * 4, percent)?; + } + main.word(0x2544, 4)?; + main.word(0x2548, 6)?; + main.word(0x254c, 3)?; + main.word(0x2550, 7)?; + main.word(0x2554, 7)?; + main.word(0x2568, 10)?; + main.word(0x2570, 1)?; + main.word(0x258c, 0x160000)?; + main.word(0x2594, 1)?; + main.word(0x2598, 1)?; + main.word(0x259c, 1)?; + main.word(0x25a4, 1)?; + main.word(0x25a8, 1)?; + main.word(0x25ac, 1)?; + main.0[0x25b4..0x25e4].fill(0xff); + main.word(0x2604, 1)?; + main.word(0x2634, 1)?; + + globals.word(0x24, 3000)?; + globals.word(0x78, 1)?; + globals.word(0x84, budget)?; + for offset in [0x88, 0x8c, 0x98] { + globals.word(offset, ceiling)?; + } + globals.word(0x998, 40)?; + globals.word(0x99c, 10)?; + globals.word(0x9a0, 250)?; + globals.word(0x9b8, 2)?; + globals.word(0x9bc, 40)?; + globals.word(0x9c0, 5)?; + globals.word(0x9c8, 40)?; + globals.word(0x9cc, 50)?; + globals.word(0xe28, 3)?; + + power.word(0x10, average_periods)?; + power.float(0x14, pwr.avg_power_ki_only * dt)?; + power.float(0x18, pwr.avg_power_kp)?; + power.word(0x1c, pwr.avg_power_min_duty_cycle)?; + power.word(0x20, pwr.avg_power_target_filter_tc)?; + power.word(0xc8, 11_000)?; + power.word(0xcc, 8_000)?; + power.float(0xd8, pwr.fast_die0_proportional_gain)?; + power.float(0xdc, pwr.fast_die0_integral_gain * dt)?; + power.word(0x12c, budget)?; + power.float(0x130, pwr.ppm_kp)?; + power.float(0x134, pwr.ppm_ki * dt)?; + power.word(0x14c, 1)?; + + performance.word(4, pwr.pwr_sample_period_aic_clks)?; + performance.word(8, pwr.pwr_sample_period_aic_clks)?; + performance.word(0x10, 4)?; + performance.float(0x14, f32!(1.0))?; + performance.word(0x2c, 1)?; + performance.word(0x30, 1)?; + // Leave the observed hardware state at zero until firmware writes it. + for offset in [0x40, 0x80, 0xb78, 0xb80, 0xbc0] { + performance.word(offset, base_units)?; + } + for offset in [0x44, 0x6c, 0x70, 0x84] { + performance.word(offset, 1)?; + } + for offset in [ + 0x48, 0x88, 0x9f4, 0x9f8, 0xa38, 0xaac, 0xaf0, 0xb7c, 0x221c, 0x22b4, 0x22b8, 0x22f8, + 0x2870, 0x29f8, 0x29fc, 0x2a3c, 0x2b88, 0x2c44, 0x2c88, + ] { + performance.word(offset, ceiling)?; + } + performance.word(0x50, 100)?; + performance.word(0x60, 4)?; + performance.float(0x64, f32!(1.0))?; + performance.word(0x90, 100)?; + performance.word(0x9b8, 625)?; + performance.reciprocal(0x9c4, 0x9cc, pwr.pwr_filter_time_constant)?; + performance.float(0x9d4, pwr.pwr_integral_gain)?; + performance.float(0x9dc, F32::from(pwr.pwr_integral_min_clamp))?; + performance.float(0x9e0, F32::from(budget))?; + performance.float(0x9e4, pwr.pwr_proportional_gain)?; + performance.float(0x9ec, -F32::from(ceiling) / F32::from(budget))?; + for offset in [0x9f0, 0xaa8, 0x22b0] { + performance.word(offset, pwr.pwr_min_duty_cycle)?; + } + for offset in [0xa04, 0xabc, 0x28c8, 0x28cc, 0x28d0, 0x2a08] { + performance.word(offset, budget)?; + } + performance.word(0xa70, ppm_periods.checked_mul(4).ok_or(EOVERFLOW)?)?; + performance.reciprocal(0xa7c, 0xa84, ppm_periods)?; + performance.float(0xa8c, pwr.ppm_ki * dt)?; + performance.float(0xa98, f32!(65536.0))?; + performance.float(0xa9c, pwr.ppm_kp)?; + performance.word(0xac8, pwr.ppm_filter_time_constant_ms)?; + performance.word(0xad0, clocks(ppm_periods)?)?; + performance.word(0xb28, pwr.perf_tgt_utilization)?; + performance.word(0xb30, pwr.perf_boost_min_util)?; + performance.word(0xb34, pwr.perf_boost_ce_step)?; + performance.word(0xb38, pwr.perf_reset_iters)?; + performance.word(0xb40, 6)?; + performance.word(0xb44, 1)?; + performance.word(0xb48, pwr.perf_filter_drop_threshold)?; + performance.reciprocal(0xb4c, 0xb54, pwr.perf_filter_time_constant)?; + performance.reciprocal(0xb50, 0xb58, pwr.perf_filter_time_constant2)?; + performance.float(0xb5c, pwr.perf_integral_gain)?; + performance.float(0xb60, pwr.perf_integral_gain2)?; + performance.float(0xb64, F32::from(pwr.perf_integral_min_clamp))?; + performance.float(0xb68, f32!(95.0))?; + performance.float(0xb6c, pwr.perf_proportional_gain)?; + performance.float(0xb70, pwr.perf_proportional_gain2)?; + performance.float(0xb74, F32::from(8 - pwr.perf_base_pstate) / f32!(0.95))?; + performance.word(0xb8c, pwr.perf_tgt_utilization)?; + performance.float(0x21f0, f32!(65536.0))?; + performance.float(0x21f4, F32::from(pwr.pwr_min_duty_cycle))?; + performance.float(0x21f8, F32::from(ceiling))?; + performance.word(0x21fc, 1338)?; + performance.float(0x2218, f32!(100.0))?; + performance.float(0x2224, f32!(0.8))?; + performance.float(0x2228, f32!(0.2))?; + performance.word(0x2268, 0x4248)?; + performance.word(0x2270, 8_000)?; + performance.word(0x2278, 4)?; + performance.float(0x228c, f32!(1.0))?; + performance.float(0x2294, pwr.fast_die0_integral_gain * dt)?; + performance.float(0x22a0, f32!(65536.0))?; + performance.float(0x22a4, pwr.fast_die0_proportional_gain)?; + performance.word(0x22c4, 11_000)?; + performance.word(0x28c0, 1)?; + performance.reciprocal(0x28dc, 0x28e0, pwr.avg_power_target_filter_tc)?; + performance.word( + 0x28e4, + pwr.avg_power_target_filter_tc + .checked_mul(4) + .ok_or(EOVERFLOW)?, + )?; + performance.word( + 0x28e8, + pwr.avg_power_target_filter_tc + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word(0x28ec, clocks(pwr.avg_power_target_filter_tc)?)?; + performance.word(0x29bc, average_periods.checked_mul(4).ok_or(EOVERFLOW)?)?; + performance.reciprocal(0x29c8, 0x29d0, average_periods)?; + performance.float(0x29d8, pwr.avg_power_ki_only * dt)?; + performance.float(0x29e4, f32!(65536.0))?; + performance.float(0x29e8, pwr.avg_power_kp)?; + performance.word(0x29f4, pwr.avg_power_min_duty_cycle)?; + performance.float(0x2a04, F32::from(budget))?; + performance.word(0x2a14, pwr.avg_power_filter_tc_ms)?; + performance.word(0x2a1c, clocks(average_periods)?)?; + performance.word(0x2b4c, 50)?; + performance.word(0x2b50, 1)?; + performance.reciprocal(0x2b58, 0x2b60, pwr.se_filter_time_constant)?; + performance.reciprocal(0x2b5c, 0x2b64, pwr.se_filter_time_constant_1)?; + performance.float(0x2b68, pwr.se_ki * dt)?; + performance.float(0x2b6c, pwr.se_ki_1 * dt)?; + performance.float(0x2b74, f32!(65536.0))?; + performance.float(0x2b78, pwr.se_kp)?; + performance.float(0x2b7c, pwr.se_kp_1)?; + performance.word(0x2b84, 100)?; + performance.word(0x2b8c, 100)?; + performance.word(0x2b98, pwr.se_target)?; + performance.word( + 0x2ba4, + pwr.se_filter_time_constant + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word( + 0x2ba8, + pwr.se_filter_time_constant_1 + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word(0x2bac, clocks(pwr.se_filter_time_constant)?)?; + performance.word(0x2bb4, clocks(pwr.se_filter_time_constant_1)?)?; + performance.float(0x2b94, f32!(8000.0))?; + for offset in [0x2bc0, 0x2bc4, 0x2c30] { + performance.float(offset, f32!(65536.0))?; + } + performance.word(0x2be8, 8000)?; + performance.word(0x2bec, 4)?; + performance.word(0x2bf0, pwr.se_inactive_threshold)?; + performance.word(0x2bf4, pwr.se_engagement_criteria as u32)?; + performance.word(0x2bf8, 2)?; + performance.word(0x2bfc, 4)?; + performance.word(0x2c00, pwr.se_reset_criteria)?; + performance.word(0x2c40, pwr.avg_power_min_duty_cycle)?; + performance.word(0x2e2c, 4)?; + performance.float(0x2e38, f32!(0.96875))?; + performance.float(0x2e40, f32!(0.03125))?; + performance.float(0x2e48, f32!(5.0))?; + performance.float(0x2e54, f32!(65536.0))?; + performance.float(0x2e58, f32!(15.0))?; + performance.word(0x2e64, 700)?; + for offset in [0x2e68, 0x2e6c, 0x2e78] { + performance.word(offset, ceiling)?; + } + performance.word(0x2f08, 0x4248)?; + performance.word(0x2f10, 125)?; + performance.word(0x2f14, 1)?; + performance.pointer(0x3b50, sensor)?; + performance.word(0x3b58, 0x4b48)?; + performance.word(0x3b60, 1)?; + performance.word(0x62a8, 1)?; + Ok(Self { + hardware: main.0, + performance: performance.0, + power: power.0, + globals: globals.0, + maximum_khz: top.freq_hz / 1000, + }) + } +} diff --git a/drivers/gpu/drm/asahi/m3/mod.rs b/drivers/gpu/drm/asahi/m3/mod.rs index 7073ecf294d631..d885da1a8647ca 100644 --- a/drivers/gpu/drm/asahi/m3/mod.rs +++ b/drivers/gpu/drm/asahi/m3/mod.rs @@ -202,3 +202,8 @@ mod t8122_hwdata; mod m3_init_26; mod m3_profile; mod m3_mmio_26; + +#[cfg(CONFIG_DRM_ASAHI_M3_T8122)] +mod m3_power_26; + +mod m3_channel_format; diff --git a/tools/asahi/m3-runtime-tests.rs b/tools/asahi/m3-runtime-tests.rs index f1768a242147b6..abac06668c0a1c 100644 --- a/tools/asahi/m3-runtime-tests.rs +++ b/tools/asahi/m3-runtime-tests.rs @@ -24,3 +24,15 @@ mod m3_profile; #[path = "../../drivers/gpu/drm/asahi/m3/m3_mmio_26.rs"] mod m3_mmio_26; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_queue_layout.rs"] +mod m3_queue_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_pool_layout.rs"] +mod m3_pool_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_compute_layout.rs"] +mod m3_compute_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_channel_format.rs"] +mod m3_channel_format; diff --git a/tools/asahi/test-m3-power.py b/tools/asahi/test-m3-power.py new file mode 100644 index 00000000000000..51bea088cd9db5 --- /dev/null +++ b/tools/asahi/test-m3-power.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run the production controller writer and soft-float code without a GPU. + +Kernel allocation/DT adapters are replaced by host adapters. Public production +power types and PStateTables are extracted as complete balanced Rust items, +never restated by hand. Their input hashes are printed with the test evidence. +""" +from pathlib import Path +import hashlib +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +M3 = ROOT / "drivers/gpu/drm/asahi/m3" + + +def item(source, anchor): + start = source.index(anchor) + opening = source.index("{", start) + depth = 1 + end = opening + 1 + while depth: + if source[end] == "{": + depth += 1 + elif source[end] == "}": + depth -= 1 + end += 1 + return source[start:end] + + +shim = r''' +#![allow(dead_code)] +extern crate self as kernel; +pub type Error = i32; +pub type Result = core::result::Result; +pub const EINVAL: Error = 22; +pub const EOVERFLOW: Error = 75; +pub const ERANGE: Error = 34; +pub const GFP_KERNEL: () = (); +pub unsafe trait Zeroable {} +#[derive(Debug)] +pub struct KVec(Vec); +impl Default for KVec { fn default() -> Self { Self(Vec::new()) } } +impl KVec { + pub fn new() -> Self { Self::default() } + pub fn resize(&mut self, n: usize, value: T, _: ()) -> Result where T: Clone { + self.0.resize(n, value); Ok(()) + } +} +impl core::ops::Deref for KVec { type Target = Vec; fn deref(&self) -> &Vec { &self.0 } } +impl core::ops::DerefMut for KVec { fn deref_mut(&mut self) -> &mut Vec { &mut self.0 } } +pub mod prelude { pub use crate::{Error,Result,EINVAL,EOVERFLOW,ERANGE,GFP_KERNEL,KVec,Zeroable}; } +pub mod of { + pub struct Property<'a>(pub &'a [u8]); + pub trait PropertyUnit: Sized { const UNIT_SIZE: usize; fn from_bytes(data: &[u8]) -> crate::Result; } + impl PropertyUnit for u32 { + const UNIT_SIZE: usize = 4; + fn from_bytes(data: &[u8]) -> crate::Result { + Ok(u32::from_be_bytes(data.try_into().map_err(|_| crate::EINVAL)?)) + } + } + impl TryFrom> for u32 { + type Error = crate::Error; + fn try_from(value: Property<'_>) -> crate::Result { Self::from_bytes(value.0) } + } +} +''' +fixture = r''' +#[cfg(test)] mod controller_tests { + use super::*; + use float::F32; + fn input() -> hw::PwrConfig { + let mut p = hw::PwrConfig::default(); + p.perf_states = KVec((0..9).map(|i| hw::PState { + freq_hz: if i == 8 { 1_338_000_000 } else { i * 100_000_000 }, + volt_mv: KVec(vec![if i == 0 { 125 } else { 450 + i * 50 }]), pwr_mw: i * 1000, + }).collect()); + p.perf_base_pstate = 1; + p.power_sample_period = 8; + p.ppm_filter_time_constant_ms = 16; + p.avg_power_filter_tc_ms = 16; + p.pwr_filter_time_constant = 2; + p.perf_filter_time_constant = 2; + p.perf_filter_time_constant2 = 2; + p.avg_power_target_filter_tc = 2; + p.se_filter_time_constant = 2; + p.se_filter_time_constant_1 = 2; + p.pwr_min_duty_cycle = 30; + p.avg_power_min_duty_cycle = 20; + p + } + fn build(p: &hw::PwrConfig, sram: &[u32]) -> Result { + m3_power_26::PowerImages::new(p, sram, 0xffff_fc21_0340_0000, + 0xffff_fc21_0350_0000, 0xffff_fc2f_fc00_0000, &[0; 53 * 40]) + } + fn word(b: &[u8], at: usize) -> u32 { u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) } + #[test] fn native_controller_bounds_are_distinct_from_outputs_and_hardware_state() { + let image = build(&input(), &[950_001; 9]).unwrap(); + assert_eq!(image.hardware.len(), 0x2658); + assert_eq!(image.performance.len(), 0x6578); + assert_eq!(image.power.len(), 0x670); + assert_eq!(image.globals.len(), 0xe48); + assert_eq!(image.maximum_khz, 1_338_000); + assert_eq!(word(&image.performance, 0x2c), 1); + assert_eq!(word(&image.performance, 0x30), 1); + assert_eq!(word(&image.performance, 0x38), 0); + assert_eq!(word(&image.performance, 0x9f0), 30); + assert_eq!(word(&image.performance, 0xa38), 800); + assert_eq!(word(&image.performance, 0xb78), 100); + assert_eq!(word(&image.performance, 0xb7c), 800); + assert_eq!(word(&image.hardware, 0x1408 + 64), 951); + assert_eq!(word(&image.performance, 0x2268), 0x4248); + assert_eq!(word(&image.performance, 0x3b58), 0x4b48); + } + #[test] fn malformed_tunables_and_nonfinite_gains_are_rejected() { + for period in [0, 1001, u32::MAX] { + let mut p = input(); p.power_sample_period = period; + assert!(build(&p, &[950_000; 9]).is_err()); + } + for bits in [0x7f800000, 0xff800000, 0x7fc00001] { + let mut p = input(); p.pwr_integral_gain = F32::from_bits(bits); + assert!(build(&p, &[950_000; 9]).is_err()); + } + assert!(build(&input(), &[950_000; 8]).is_err()); + assert!(build(&input(), &[100_000; 9]).is_err()); + let mut p = input(); p.avg_power_target_filter_tc = u32::MAX; + assert!(build(&p, &[950_000; 9]).is_err()); + } + #[test] fn dt_order_is_not_the_firmware_voltage_order() { + let p = input(); + let expected = build(&p, &[950_000; 9]).unwrap(); + let mut shuffled = input(); + shuffled.perf_states.swap(2, 6); + let actual = build(&shuffled, &[950_000; 9]).unwrap(); + assert_eq!(actual.hardware.0, expected.hardware.0); + assert_eq!(actual.performance.0, expected.performance.0); + } +} +''' + + +def main(): + hw = (M3 / "hw.rs").read_text() + init = (M3 / "initdata.rs").read_text() + code = shim + # Keep the production soft-float prefix verbatim. Its unrelated existing + # print-only TODO test lacks an import and is not part of this harness. + soft_float = (ROOT / "drivers/gpu/drm/asahi/float.rs").read_text() + code += "mod float {\n" + soft_float.split("// TODO: Make this an actual test", 1)[0] + "\n}\n" + code += "mod hw { use crate::{KVec,float::F32};\n" + for name in ["PState", "PowerZone", "CsAfrPwrConfig", "PwrConfig"]: + code += "#[derive(Default)]\n" + item(hw, "pub(crate) struct " + name + " {") + "\n" + code += item(hw, "impl PState {") + "\n" + code += "}\nmod initdata { use crate::{prelude::*,hw}; const PSTATE_TABLE_ENTRIES: usize = 16;\n" + code += item(init, "pub(crate) struct PStateTables {") + "\n" + code += item(init, "impl PStateTables {") + "\n}\n" + code += "mod m3_mmio_26 { pub const SLOTS:usize=53; pub const RECORD_BYTES:usize=40; }\n" + code += f'#[path = "{M3}/m3_power_26.rs"] mod m3_power_26;\n' + code += fixture + for path in [M3 / "hw.rs", M3 / "initdata.rs", M3 / "m3_power_26.rs", ROOT / "drivers/gpu/drm/asahi/float.rs"]: + print(hashlib.sha256(path.read_bytes()).hexdigest(), path.relative_to(ROOT), flush=True) + with tempfile.TemporaryDirectory(prefix="m3-power-") as directory: + source = Path(directory) / "test.rs" + source.write_text(code) + for flags in [[], ["-O"]]: + binary = Path(directory) / "tests" + subprocess.run(["rustc", "--edition=2021", "--test", *flags, str(source), "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index 4d2fbc3c53dafb..1b51602b132cec 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2326,6 +2326,15 @@ "tools/asahi/test-m3-uapi.py" ], "rust": "1.85.0" + }, + { + "name": "tools/asahi/test-m3-power.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-power.py" + ], + "rust": "1.85.0" } ], "helpers": { From ff9acb9b030c5dbf1690849e02a5595dd4b757f6 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:40:41 -0400 Subject: [PATCH 06/15] drm/asahi: prepare extended firmware queues and controller handoff Construct native queue, pool, context and notifier records. Read controller inputs from the admitted OPP handoff, preserve nanosecond durations and reject unsupported optional flags. Prepare native startup without enabling extended admission. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/m3/m3_adt_config.rs | 57 +++++++- drivers/gpu/drm/asahi/m3/m3_compute_layout.rs | 21 +++ .../gpu/drm/asahi/m3/m3_compute_sequence.rs | 110 +++++++++++++++ .../gpu/drm/asahi/m3/m3_compute_storage.rs | 66 +++++++++ drivers/gpu/drm/asahi/m3/m3_config.rs | 79 ++++++++++- drivers/gpu/drm/asahi/m3/m3_device.rs | 6 + drivers/gpu/drm/asahi/m3/m3_drm.rs | 3 +- drivers/gpu/drm/asahi/m3/m3_pool.rs | 31 +++++ drivers/gpu/drm/asahi/m3/m3_pool_layout.rs | 128 ++++++++++++++++++ drivers/gpu/drm/asahi/m3/m3_profile.rs | 20 +++ drivers/gpu/drm/asahi/m3/m3_queue_layout.rs | 41 ++++++ drivers/gpu/drm/asahi/m3/m3_runtime.rs | 57 ++++++++ drivers/gpu/drm/asahi/m3/m3_state_layout.rs | 52 +++++++ drivers/gpu/drm/asahi/m3/m3_submit.rs | 8 +- drivers/gpu/drm/asahi/mmu.rs | 4 +- tools/asahi/m3-runtime-tests.rs | 10 ++ 16 files changed, 678 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs index 1893c113cf2a7d..355cd44b040254 100644 --- a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs @@ -260,7 +260,14 @@ impl PstatePolicy { /// The runtime backend's InitData contents: the generated object images and the /// performance-state policy. +pub(crate) struct ExtendedInputs { + pub(crate) power: hw::PwrConfig, + pub(crate) sram_uv: KVec, + pub(crate) bases: super::m3_mmio_26::RegisterBases, +} + pub(crate) struct Contents { + pub(crate) extended: Option, /// Generated images, per owner; None for owners that stay zero. Only the three image /// owners' images are uploaded; the records' are checked. images: KVec>>, @@ -304,6 +311,10 @@ impl Contents { experiment: Option<&Experiment>, ) -> Result { let dev = pdev.as_ref(); + if firmware.profile() == super::m3_profile::Profile::Extended { + if experiment.is_some() { return Err(EINVAL); } + return Self::extended(dev, soc); + } let param = m3_params::initdata_param(); check_version(dev, firmware, experiment)?; let iomaps = match experiment { @@ -370,12 +381,54 @@ impl Contents { ); let read_only_slots = experiment.map(|e| e.read_only_slots()).unwrap_or(0); let hwdata_object = soc.hwdata_object; - Ok(Contents { images, pstates, iomaps, table, read_only_slots, hwdata_object }) + Ok(Contents { extended: None, images, pstates, iomaps, table, read_only_slots, hwdata_object }) + } + + fn extended(dev: &device::Device, soc: &Soc) -> Result { + let cfg = soc.hwcfg.ok_or(ENODEV)?; + if soc.chip_id != 0x8122 || cfg.chip_id != 0x8122 { return Err(ENODEV); } + let power = hw::PwrConfig::load(dev, cfg)?; + let tables = initdata::PStateTables::new(&power, true, None, false)?; + if tables.len != 9 || tables.max != 8 || tables.max_frequency_khz(&power) != 1_338_000 { + return Err(EINVAL); + } + let node = dev.of_node().ok_or(ENODEV)?; + let opps = node.parse_phandle(c_str!("operating-points-v2"), 0).ok_or(EINVAL)?; + let mut sram_uv = KVec::new(); + for opp in opps.children() { + let values: KVec = opp.get_property(c_str!("opp-microvolt-sram"))?; + if values.len() != 1 { return Err(EINVAL); } + sram_uv.push(values[0], GFP_KERNEL)?; + } + if sram_uv.len() != power.perf_states.len() { return Err(EINVAL); } + let block = |index: usize| -> Result { + Ok(cfg.io_mappings.get(index).and_then(|value| value.as_ref()).ok_or(EINVAL)?.base as u64) + }; + let bases = super::m3_mmio_26::RegisterBases { + gpu: soc.windows.sgx, interrupt: block(2)?, + memory: block(11)?.checked_add(0x1c4000).ok_or(EOVERFLOW)?, + telemetry_write: block(18)?, telemetry_read: block(19)?, + neural: block(25)?, metrology: block(26)?, + }; + let mut freqs = [0; 16]; + for (index, value) in freqs.iter_mut().enumerate().take(tables.len) { + *value = tables.primary_state(&power, index).freq_hz / 1_000_000; + } + // This profile enables the native firmware's thermal/PID controllers. + // The legacy host-only thermal governor must not replace those inputs. + let pstates = PstatePolicy { table_max: 8, table_max_mhz: 1338, + max: 8, safe: 8, boot: 1, max_mhz: 1338, safe_mhz: 1338, + boot_mhz: freqs[1], freqs, thermal: ThermalMode::Off }; + let mut images = KVec::new(); + for _ in 0..super::m3_init_26::COUNT { images.push(None, GFP_KERNEL)?; } + Ok(Self { extended: Some(ExtendedInputs { power, sram_uv, bases }), + images, pstates, iomaps: &[], table: (8, 1338), read_only_slots: 0, hwdata_object: None }) } /// Check the contents before they are uploaded. pub(crate) fn check_for_upload(&self) -> Result { - if self.images.len() == storage::COUNT { + let count = if self.extended.is_some() { super::m3_init_26::COUNT } else { storage::COUNT }; + if self.images.len() == count { Ok(()) } else { Err(EINVAL) diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs index 64c8cb005c4a1a..ace58e38f38a6d 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs @@ -71,7 +71,14 @@ impl Command { /// Firmware envelopes vary independently of the GPU's CDM stream format. pub(crate) fn encode_for(&self, profile: super::m3_profile::Profile, asid_generation: u8, event_slot: u8, out: &mut [u8]) -> Result<(), Error> { + self.encode_with_usc(profile, asid_generation, event_slot, 0, out) + } + + pub(crate) fn encode_with_usc(&self, profile: super::m3_profile::Profile, + asid_generation: u8, event_slot: u8, usc_base: u64, out: &mut [u8]) -> Result<(), Error> { let extended = profile == super::m3_profile::Profile::Extended; + if usc_base & 0xffff_ffff != 0 || usc_base >= 1 << 42 + || (!extended && usc_base != 0) { return Err(Error::Address); } let size = if extended { EXTENDED_SIZE } else { SIZE }; if out.len() < size { return Err(Error::Size); } if extended && (self.register_set != RegisterSet::G15G || event_slot >= 4 @@ -122,6 +129,7 @@ impl Command { emit(0x1a540, 1); emit(0x014a9, 1); emit(0x0a351, 1); + if usc_base != 0 { emit(0x10071, usc_base); } } pointer(bytes, 0x720, registers.get()); bytes[0x728..0x72a].copy_from_slice(&(count as u16).to_le_bytes()); @@ -232,4 +240,17 @@ mod tests { assert_eq!(read64(&b, META), c.stamp.get()); assert_eq!(&b[SIZE..], &[0xa5; EXTENDED_SIZE - SIZE]); } + #[test] + fn optional_usc_window_is_a_dispatch_register_without_stream_rewriting() { + let c = fixture(); + let mut output = [0xa5; EXTENDED_SIZE]; + c.encode_with_usc(Profile::Extended, 1, 0, 1 << 32, &mut output).unwrap(); + assert_eq!(read32(&output, 0x728) & 0xffff, 25); + assert_eq!(read32(&output, REGISTERS + 24 * REGISTER_STRIDE), 0x10071); + assert_eq!(read64(&output, REGISTERS + 24 * REGISTER_STRIDE + 4), 1 << 32); + output.fill(0xa5); + assert!(c.encode_with_usc(Profile::Extended, 1, 0, 1, &mut output).is_err()); + assert_eq!(output, [0xa5; EXTENDED_SIZE]); + } + } diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs b/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs index a10c3040d570f0..bd78c03823aa07 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs @@ -82,3 +82,113 @@ impl Sequence { Ok(()) } } + +impl Sequence { + pub(crate) const EXTENDED_LENGTH: usize = 0x300; + pub(crate) const EXTENDED_ATTACHMENTS: usize = 0x58; + pub(crate) const EXTENDED_HAS_ATTACHMENTS: usize = 0x2c0; + + /// The extended firmware operates on a native G15 CDM stream. Its firmware + /// microsequence has wider timestamp operations and different engine state. + pub(crate) fn encode_extended(&self, auxiliary: command::GpuVa, + context_store: super::m3_pool_layout::GpuRegion, queue_sequence: u64, + output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 || self.generation != 0 { return Err(Error::Address); } + if context_store.size() < 0x20000 { return Err(Error::Size); } + let registers = self.command.at(command::REGISTERS, 24 * command::REGISTER_STRIDE)?; + let engine = self.command.at(0x76c, 0x60)?; + let clock = self.command.at(0x82c, 8)?; + let pairs = self.command.at(0x834, 16)?; + let end_pair = self.command.at(0x83c, 8)?; + let pool_pointer = self.command.at(0x86a, 8)?; + let status = self.command.at(0x8ac, 4)?; + let time_aux = self.command.at(0x89c, 8)?; + let elapsed = self.command.at(0x7dc, 8)?; + let tail = self.command.at(0x88c, 8)?; + let activity = self.stats.at(0, 8)?; + let bytes = output.get_mut(..STORAGE).ok_or(Error::Size)?; + bytes.fill(0); + word(bytes, 0, 0x0b); + pointer(bytes, 0x14, registers); + pointer(bytes, 0x1c, activity); + pointer(bytes, 0x24, self.queue.get()); + word(bytes, 0x2c, self.context); + word(bytes, 0x30, 1); + pointer(bytes, 0x38, queue_sequence); + pointer(bytes, 0x44, engine); + word(bytes, 0x50, 1); + pointer(bytes, 0x160, self.pool.get()); + pointer(bytes, 0x178, auxiliary.get()); + for (offset, delta) in [0, 0xf400, 0x1e800, 0x1f000].into_iter().enumerate() { + pointer(bytes, 0x188 + offset * 8, context_store.base() + delta); + } + pointer(bytes, 0x1a8, status); + word(bytes, 0x1b8, 2); + // Timestamp records are 76 bytes; WFI sits between the two records. + for (offset, opcode, selected) in [(0x1bc, 0x8000_0003, pairs), (0x20c, 3, end_pair)] { + word(bytes, offset, opcode); + pointer(bytes, offset + 4, clock); + pointer(bytes, offset + 0xc, pairs); + pointer(bytes, offset + 0x14, selected); + pointer(bytes, offset + 0x1c, self.queue.get()); + pointer(bytes, offset + 0x2c, time_aux); + pointer(bytes, offset + 0x34, elapsed); + pointer(bytes, offset + 0x44, 0xc10000); + } + word(bytes, 0x208, 1); + let finish = 0x258; + word(bytes, finish, 0x0c); + pointer(bytes, finish + 4, activity); + pointer(bytes, finish + 0xc, self.queue.get()); + word(bytes, finish + 0x14, self.context); + pointer(bytes, finish + 0x18, engine); + pointer(bytes, finish + 0x24, 0xc10000); + pointer(bytes, finish + 0x2c, self.fw_stamp.get()); + word(bytes, finish + 0x34, self.stamp_value); + pointer(bytes, finish + 0x5c, pool_pointer); + word(bytes, finish + 0x64, (-(finish as i32)) as u32); + pointer(bytes, finish + 0x69, status); + pointer(bytes, finish + 0x71, tail); + word(bytes, 0x2d4, 0x4000_0002); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + fn fixture() -> Sequence { + let fw = |i: u64| FirmwareVa::new(0xffff_fc21_a000_0000 + i * 0x4000).unwrap(); + Sequence { command: Region::new(fw(0).get(), command::EXTENDED_SIZE).unwrap(), + stats: Region::new(fw(1).get(), 0xe10).unwrap(), + notifier: Region::new(fw(2).get(), 0x200).unwrap(), queue: fw(3), + context: 3, generation: 0, pool: fw(4), scratch: [fw(5); 5], + fw_stamp: fw(6), stamp_value: 0x700 } + } + fn ptr(bytes: &[u8], at: usize) -> u64 { u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()) } + #[test] fn extended_timestamps_and_retirement_refer_to_their_live_owners() { + let s = fixture(); + let mut output = [0xa5; STORAGE + 8]; + s.encode_extended(command::GpuVa::new(0x5000_0000).unwrap(), + super::super::m3_pool_layout::GpuRegion::new(0x6000_0000, 0x20000).unwrap(), 7, &mut output).unwrap(); + assert_eq!(ptr(&output, 0x1c), s.stats.at(0, 8).unwrap()); + assert_eq!(ptr(&output, 0x38), 7); + assert_eq!(ptr(&output, 0x188), 0x6000_0000); + assert_eq!(ptr(&output, 0x1bc + 0x14), s.command.at(0x834, 8).unwrap()); + assert_eq!(ptr(&output, 0x20c + 0x14), s.command.at(0x83c, 8).unwrap()); + assert_eq!(ptr(&output, 0x258 + 0x2c), s.fw_stamp.get()); + assert_eq!(ptr(&output, 0x258 + 0x5c), s.command.at(0x86a, 8).unwrap()); + assert_eq!(&output[STORAGE..], &[0xa5; 8]); + } + #[test] fn extended_sequence_rejects_legacy_command_extents_and_event_generations() { + for generation in [0, 1] { + let mut s = fixture(); + s.generation = generation; + if generation == 0 { s.command = Region::new(s.command.at(0, 1).unwrap(), command::SIZE).unwrap(); } + let mut output = [0xa5; STORAGE]; + assert!(s.encode_extended(command::GpuVa::new(0x5000_0000).unwrap(), + super::super::m3_pool_layout::GpuRegion::new(0x6000_0000, 0x20000).unwrap(), 0, &mut output).is_err()); + assert_eq!(output, [0xa5; STORAGE]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs b/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs index 6bd3aecda387d3..a897a45f0922de 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs @@ -87,3 +87,69 @@ pub(crate) fn allocation(index:usize)->Result { pub(crate) fn coalesce_stamp_flush(wide:bool,timestamps:[u64;2],last:bool)->bool { wide && timestamps==[0,0] && !last } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, index: usize) + -> Result { + let mut owner = allocation(index)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + match index { + // The initial allocator has eight 4-MiB payload owners. Retain the + // historical indices without allocating unused 4-MiB payloads. + 9..=22 => { owner.size = 0x4000; owner.mapping = Mapping::FirmwareOnly; } + POOL_PAGES => owner.size = 0x20000, + POOL_TABLE => { owner.size = 0x4000; owner.mapping = Mapping::Client(POOL_TABLE_VA & !0x3fff); } + POOL => owner.size = 0xa0, + GPU_CONTEXT => owner.size = 0x38, + NOTIFIER => owner.size = 0x200, + EVENT => owner.size = 8, + QUEUE => owner.size = 0x24c0, + PREEMPTION => owner.size = 0x4000, + COMMAND => { owner.size = 0x8b0; owner.mapping = Mapping::CommandAlias(0x73_0000_0000); } + TAIL_SCRATCH => { owner.size = 0x4000; owner.mapping = Mapping::Client(0x72_0100_0000); } + 46 => { owner.size = 0x40000; owner.mapping = Mapping::Client(0x72_0000_0000); } + 45 | 47..=49 => { owner.size = 0x4000; owner.mapping = Mapping::FirmwareOnly; } + _ => {} + } + Ok(owner) +} + +pub(crate) fn slot_offset_for(profile: super::m3_profile::Profile, index: usize, slot: usize) + -> Result { + if slot >= SLOTS { return Err(InvalidOwner); } + let size = allocation_for(profile, index)?.size; + Ok(if RESET.contains(&index) { ((size + 0x3fff) & !0x3fff) * slot } else { 0 }) +} + +pub(crate) fn batch_allocation_for(profile: super::m3_profile::Profile, index: usize) + -> Result { + let mut owner = allocation_for(profile, index)?; + owner.size += slot_offset_for(profile, index, SLOTS - 1)?; + Ok(owner) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + use super::super::m3_profile::Profile; + #[test] fn extended_commands_do_not_straddle_another_slots_page() { + let command = allocation_for(Profile::Extended, COMMAND).unwrap(); + let Mapping::CommandAlias(base) = command.mapping else { panic!("missing command alias") }; + assert_eq!(base & 0x3fff, 0); + for slot in 0..SLOTS - 1 { + let start = slot_offset_for(Profile::Extended, COMMAND, slot).unwrap(); + let next = slot_offset_for(Profile::Extended, COMMAND, slot + 1).unwrap(); + assert!(start + command.size <= next); + } + assert_eq!(batch_allocation_for(Profile::Extended, QUEUE).unwrap().size, 0x24c0); + assert!(slot_offset_for(Profile::Extended, COMMAND, SLOTS).is_err()); + } + #[test] fn legacy_plan_is_byte_for_byte_the_same_for_every_owner_and_slot() { + for index in 0..COUNT { + assert_eq!(allocation_for(Profile::Legacy, index), allocation(index)); + for slot in 0..SLOTS { + assert_eq!(slot_offset_for(Profile::Legacy, index, slot), slot_offset(index, slot)); + } + assert_eq!(batch_allocation_for(Profile::Legacy, index), batch_allocation(index)); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_config.rs b/drivers/gpu/drm/asahi/m3/m3_config.rs index 67f4f963637f23..bfb408b07a5307 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -51,6 +51,7 @@ pub(crate) struct Config { objects: KVec, profile: super::m3_profile::Profile, control_generation: u32, + io_descriptors: Option>, _iomaps: KVec, pub(crate) completed_events: u64, pstates: crate::m3::m3_adt_config::PstateWatch, @@ -67,6 +68,17 @@ impl Config { pub(crate) fn new(dev: &driver::AsahiDrmDevice, uat: &mmu::Uat, firmware: &crate::m3::m3_firmware::Firmware, contents: &crate::m3::m3_adt_config::Contents) -> Result { contents.check_for_upload()?; + if let Some(inputs) = &contents.extended { + if firmware.profile() != super::m3_profile::Profile::Extended { return Err(EINVAL); } + let owners = ExtendedStorage::new(dev, uat, &inputs.power, &inputs.sram_uv, inputs.bases)?; + let thermal = crate::m3::m3_thermal::Governor::new(dev.as_ref().as_ref(), &contents.pstates)?; + return Ok(Self { objects: owners.objects, _iomaps: owners.iomaps, + profile: firmware.profile(), control_generation: 0, completed_events: 0, + io_descriptors: Some(owners.descriptors), + pstates: crate::m3::m3_adt_config::PstateWatch::new(contents.pstates), thermal, + request_top: 8, request_value: 0, request_checked: false, request_check_at: None, + request_writes: 0, request_observed: 0 }); + } let mut objects = KVec::new(); let mut iomaps = KVec::new(); for index in 0..storage::COUNT { @@ -142,7 +154,7 @@ impl Config { crate::m3::m3_adt_config::check_upload(dev.as_ref().as_ref(), firmware, contents.iomaps, &mut objects)?; agx_memory::publish(); let thermal = crate::m3::m3_thermal::Governor::new(dev.as_ref().as_ref(), &contents.pstates)?; - Ok(Self { objects, profile: firmware.profile(), control_generation: 0, + Ok(Self { objects, profile: firmware.profile(), control_generation: 0, io_descriptors: None, _iomaps: iomaps, completed_events: 0, pstates: crate::m3::m3_adt_config::PstateWatch::new(contents.pstates), thermal, request_top:contents.table.0,request_value:0,request_checked:false, @@ -156,6 +168,15 @@ impl Config { objects[RUNTIME_POINTERS].write(0, &records.runtime)?; objects[CONTROL_REGION].write(0, &records.control) } + pub(crate) fn publish_io_table(&self, + pdev: &kernel::platform::Device>, + firmware: &crate::m3::m3_firmware::Firmware) -> Result { + if let Some(descriptors) = &self.io_descriptors { + firmware.publish_io_table(pdev, descriptors)?; + } + Ok(()) + } + pub(crate) fn root(&self) -> u64 { self.objects[storage::ROOT].va() } pub(crate) fn log_globals_debug(&mut self,dev:&driver::AsahiDrmDevice)->Result { let value=self.objects[GLOBALS].read_u32(crate::m3::m3_globals_debug::OFFSET)?; @@ -164,6 +185,8 @@ impl Config { Ok(()) } pub(crate) fn performance_request(&mut self,chip:u32,busy:bool)->Result { + // Native controller inputs supersede the legacy fixed-request interface. + if self.profile == super::m3_profile::Profile::Extended { return Ok(()); } let Some(value)=crate::m3::m3_consistent_perf::request(chip, crate::m3::m3_params::consistent_perf(),busy,self.request_top) else {return Ok(());}; if value==0 && self.request_value==0 {return Ok(());} @@ -195,6 +218,14 @@ impl Config { /// state the firmware reports against the ceiling and the runtime cap. Called after boot and /// after every job, by the job thread. pub(crate) fn check_pstate(&mut self, dev: &driver::AsahiDrmDevice, device: &crate::m3::m3_device::Device, what: &str) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let state = device.pstate_register()? & 15; + if state > self.request_top { + dev_err!(dev.as_ref(), "M3: native performance state {} exceeds {} ({})\n", state, self.request_top, what); + return Err(ERANGE); + } + return Ok(()); + } let now = kernel::time::Instant::::now(); if let Some(cap) = self.thermal.update(dev.as_ref().as_ref(), now) { crate::m3::m3_adt_config::publish_runtime_cap(&mut self.objects[GLOBALS], cap)?; @@ -206,13 +237,25 @@ impl Config { Ok(()) } pub(crate) fn stats_region(&self) -> Result { - let owner = &self.objects[HARDWARE_DATA]; + let owner = &self.objects[if self.profile == super::m3_profile::Profile::Extended { + UNKNOWN_C3 + } else { HARDWARE_DATA }]; init::Region::new(owner.va(), owner.size()).map_err(|_| EINVAL) } pub(crate) fn log_active_power(&mut self, dev: &driver::AsahiDrmDevice, device: &crate::m3::m3_device::Device, sequence: u32) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let state = device.pstate_register()? & 15; + let performance = &mut self.objects[super::m3_init_26::PERFORMANCE_CONTROL]; + let actual = performance.read_u32(0x2c)?; + let target = performance.read_u32(0x30)?; + let hardware = performance.read_u32(0x38)?; + dev_info!(dev.as_ref(), "M3_NATIVE_POWER sample={} state={} actual={} target={} hardware={}\n", + sequence, state, actual, target, hardware); + return Ok(()); + } let cap = self.thermal.allowed(kernel::time::Instant::::now()); self.pstates.log_active(dev.as_ref().as_ref(), &mut self.objects[HARDWARE_DATA], device.pstate_register()?, cap, sequence)?; @@ -232,6 +275,7 @@ impl Config { } pub(crate) fn render_pb(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } self.objects[UNKNOWN_C1].write(0,&storage::parameter_buffer(crate::m3::m3_render::PB_PAGES).map_err(|_|EINVAL)?) } pub(crate) fn submit_queue(&mut self, kind:usize, queue:u64, head:u16,event:u8,new:bool)->Result { @@ -274,6 +318,13 @@ impl Config { && self.objects[RUNTIME_FLAGS].read_u32(4)? == 1) } pub(crate) fn log_ready(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let ready = self.objects[RUNTIME_FLAGS].read_u32(0x14)?; + let consumed = self.objects[CONTROL_REGION].read_u32(0xeae8)?; + dev_info!(dev.as_ref(), "M3: extended firmware ready={} control-generation={}/{}\n", + ready, consumed, self.control_generation); + return Ok(()); + } let magic=self.objects[RUNTIME_POINTERS].read_u32(0x314)?; let state=self.objects[RUNTIME_POINTERS].read_u32(0x2fc)?; let a=self.objects[RUNTIME_FLAGS].read_u32(4)?; @@ -284,12 +335,14 @@ impl Config { } /// The firmware's halt counter and its halted and resume flags. pub(crate) fn halt_state(&mut self) -> Result<(u64,u32,u32)> { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } let control=&mut self.objects[CONTROL_REGION]; Ok((control.read_u64(0x4580)?,control.read_u32(0x4590)?,control.read_u32(0x45a0)?)) } /// Let a halted firmware continue: clear halted, then set resume. These /// words keep the halt/resume layout of earlier Apple GPU firmware. pub(crate) fn resume_halted(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } let control=&mut self.objects[CONTROL_REGION]; control.u32(0x4590,0)?; agx_memory::publish(); @@ -303,6 +356,18 @@ impl Config { /// at +0x40a0, and the details it leaves for a recovery event at +0x44a0. /// Nothing is acknowledged, cleared or resumed here. pub(crate) fn log_recovery_state(&mut self, dev: &driver::AsahiDrmDevice, events: u64) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + for (owner, start, length) in [(super::m3_init_26::RECOVERY_ACTIVITY, 0, 0x60), + (CONTROL_REGION, 0x4ebc, 0x100)] { + for offset in (start..start + length).step_by(32) { + let mut bytes = [0; 32]; + self.objects[owner].read(offset, &mut bytes)?; + dev_err!(dev.as_ref(), "M3: firmware recovery owner={} offset={:#x} bytes={:02x?}\n", + owner, offset, bytes); + } + } + return Ok(()); + } let control=&mut self.objects[CONTROL_REGION]; let halt_count=control.read_u64(0x4580)?; let halted=control.read_u32(0x4590)?; @@ -318,7 +383,7 @@ impl Config { /// index (at most 16), as `drain` reads them. Nothing is consumed or acknowledged, and the /// completion count is not changed. pub(crate) fn log_event_ring(&mut self, dev: &driver::AsahiDrmDevice) -> Result { - const SIZE: usize = 0x38; + let size = if self.profile == super::m3_profile::Profile::Extended { 0x48 } else { 0x38 }; const COUNT: usize = 256; let r = self.objects[storage::EVENT*2].read_u32(0)? as usize; let w = self.objects[storage::EVENT*2].read_u32(0x20)? as usize; @@ -327,8 +392,8 @@ impl Config { let mut index = r; for _ in 0..16 { if index == w { break; } - let mut raw = [0u8; SIZE]; - self.objects[storage::EVENT*2+1].read(index * SIZE, &mut raw)?; + let mut raw = [0u8; 0x48]; + self.objects[storage::EVENT*2+1].read(index * size, &mut raw[..size])?; dev_info!(dev.as_ref(), "M3 event ring entry {} kind={} bytes={:02x?}\n", index, u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]), raw); index = (index + 1) % COUNT; @@ -357,7 +422,9 @@ impl Config { let state = storage::DEVICE_CONTROL * 2; let cursors = [self.objects[state].read_u32(0)?, self.objects[state].read_u32(0x10)?, self.objects[state].read_u32(0x20)?]; - let cursor = format::idle_cursor(cursors).map_err(|_| EBUSY)?; + let cursor = format::idle_cursor(cursors).map_err(|error| { + if error == init::Error::Bounds { EIO } else { EBUSY } + })?; let consumed = self.objects[CONTROL_REGION].read_u32(0xeae8)?; let generation = action.generation(self.control_generation, consumed).map_err(|_| EOVERFLOW)?; let mut record = [0; format::CONTROL_BYTES]; diff --git a/drivers/gpu/drm/asahi/m3/m3_device.rs b/drivers/gpu/drm/asahi/m3/m3_device.rs index c43017a8933c0f..05a3961369a6d2 100644 --- a/drivers/gpu/drm/asahi/m3/m3_device.rs +++ b/drivers/gpu/drm/asahi/m3/m3_device.rs @@ -176,6 +176,12 @@ impl Device { /// Read-only snapshot. Do not select or acknowledge fault banks here; /// the firmware owns those registers until recovery is ported. pub(crate) fn log_engine_state(&self, vm: &crate::mmu::Vm) -> Result { + if self.firmware.profile() == super::m3_profile::Profile::Extended { + // Adaptive clock gating can leave individual engine banks inaccessible + // even while the GPU performance-state register says powered up. + dev_info!(self.dev.as_ref(), "M3: adaptive firmware fault evidence uses retained metadata; engine banks may be gated\n"); + return Ok(()); + } let sgx_guard = self.sgx.try_access().ok_or(ENODEV)?; let sgx = sgx_guard.as_view(); let pstate = sgx.try_read32(0xe01000)? & 0xf; diff --git a/drivers/gpu/drm/asahi/m3/m3_drm.rs b/drivers/gpu/drm/asahi/m3/m3_drm.rs index d81ce66ac53aa0..4aa982ff95a5cf 100644 --- a/drivers/gpu/drm/asahi/m3/m3_drm.rs +++ b/drivers/gpu/drm/asahi/m3/m3_drm.rs @@ -258,7 +258,8 @@ impl gpu::Gpu for Backend { _ualloc_priv: Arc>, priority: u32, usc_exec_base: u64) -> Result> { Ok(KBox::new(crate::m3::m3_submit::Queue::new(self.shared.clone(),self.scheduler.clone(),vm,priority,usc_exec_base, - self.soc.features.fragment_dependency)?,GFP_KERNEL)?) + self.feature_flags() & uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_FRAGMENT_DEPENDENCY as u64 != 0, + self.feature_flags() & uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_COMPUTE_WIDE_VISIBILITY as u64 != 0)?,GFP_KERNEL)?) } fn map_timestamp_buffer(&self, bo: gem::ObjectRef, range: Range) -> Result { crate::timestamp::Buffer::firmware( diff --git a/drivers/gpu/drm/asahi/m3/m3_pool.rs b/drivers/gpu/drm/asahi/m3/m3_pool.rs index ab50ab7eca8a17..4eb284e1291bdb 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pool.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pool.rs @@ -39,3 +39,34 @@ impl Owners { objects[self.manager].write(0, &manager) } } + +impl Owners { + pub(crate) fn initialize_extended(&self, objects: &mut [Buffer], slot: u8, + compute_only: bool) -> Result { + let mut backings = KVec::new(); + for index in self.backing.clone().take(8) { + let buffer = objects.get(index).ok_or(EINVAL)?; + backings.push(wire::GpuRegion::new(buffer.gpu_va()?, buffer.size()).map_err(|_| EINVAL)?, GFP_KERNEL)?; + } + let backing = wire::ModernBacking(&backings); + let populated = backing.pages().map_err(|_| EINVAL)?; + let pages = objects.get(self.pages).ok_or(EINVAL)?; + let entries = wire::GpuRegion::new(pages.gpu_va()?, pages.size()).map_err(|_| EINVAL)?; + let table = objects.get(self.table).ok_or(EINVAL)?; + let groups = wire::GpuRegion::new(table.gpu_va()?, table.size()).map_err(|_| EINVAL)?; + if groups.size() < populated / 512 * 64 { return Err(EINVAL); } + let descriptor = wire::ModernPool { entries, groups, + submitted: FirmwareVa::new(objects.get(self.counter).ok_or(EINVAL)?.va()).map_err(|_| EINVAL)?, + identity: FirmwareVa::new(objects.get(self.manager).ok_or(EINVAL)?.va()).map_err(|_| EINVAL)?, + slot, capacity: (entries.size() / 8).try_into()?, populated: populated.try_into()?, compute_only }; + let mut bytes = [0; wire::ModernPool::SIZE]; + descriptor.encode(&mut bytes).map_err(|_| EINVAL)?; + for index in 0..populated { + objects[self.pages].u64(index * 8, backing.page(index).map_err(|_| EINVAL)?)?; + } + for index in 0..populated / 512 { + objects[self.table].write(index * 64, &backing.group(index).map_err(|_| EINVAL)?)?; + } + objects[self.manager].write(0, &bytes) + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs b/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs index a8474d262bc26d..ac7e6771f7880b 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs @@ -69,3 +69,131 @@ impl<'a> Pool<'a> { Ok(()) } } + +/// Extended firmware's private-memory pool descriptor. The owners and initial +/// population are supplied separately from the descriptor's firmware state. +pub(crate) struct ModernPool { + pub(crate) identity: FirmwareVa, + pub(crate) slot: u8, + pub(crate) entries: GpuRegion, + pub(crate) groups: GpuRegion, + pub(crate) submitted: FirmwareVa, + pub(crate) capacity: u32, + pub(crate) populated: u32, + pub(crate) compute_only: bool, +} + +impl ModernPool { + pub(crate) const SIZE: usize = 0xa0; + + pub(crate) fn encode(&self, output: &mut [u8]) -> Result<(), Error> { + let entry_bytes = (self.capacity as usize).checked_mul(8).ok_or(Error::Size)?; + let group_bytes = (self.populated as usize / 512).checked_mul(8).ok_or(Error::Size)?; + if !(1..=64).contains(&self.slot) || self.capacity == 0 || self.populated >= self.capacity || self.populated % 512 != 0 + || self.entries.base() & 0x3fff != 0 || self.groups.base() & 0x3fff != 0 + || self.entries.size() < entry_bytes || self.groups.size() < group_bytes + || self.submitted.get() & 7 != 0 || self.identity.get() & 7 != 0 { return Err(Error::Address); } + let group_count: u32 = group_bytes.try_into().map_err(|_| Error::Size)?; + let bytes = output.get_mut(..Self::SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let word = |bytes: &mut [u8], offset: usize, value: u32| { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + }; + bytes[..8].copy_from_slice(&self.identity.get().to_le_bytes()); + word(bytes, 8, self.slot.into()); + word(bytes, 12, (self.populated == 0).into()); + word(bytes, 16, 2); + bytes[0x14..0x1c].copy_from_slice(&self.entries.base().to_le_bytes()); + word(bytes, 0x1c, self.capacity); + word(bytes, 0x24, self.populated); + word(bytes, 0x28, (self.populated == 0).into()); + word(bytes, 0x2c, self.populated); + bytes[0x30..0x38].copy_from_slice(&self.groups.base().to_le_bytes()); + word(bytes, 0x40, 4); + word(bytes, 0x48, group_count); + bytes[0x4c..0x54].copy_from_slice(&self.submitted.get().to_le_bytes()); + word(bytes, 0x5c, (self.compute_only && self.populated != 0).into()); + word(bytes, 0x84, (self.populated != 0).into()); + word(bytes, 0x94, 2); + Ok(()) + } +} + +#[cfg(test)] +mod modern_tests { + use super::*; + fn pool(compute_only: bool) -> ModernPool { + ModernPool { identity: FirmwareVa::new(0xffff_fc21_9100_0000).unwrap(), slot: 1, + entries: GpuRegion::new(0x5000_0000, 0x20000).unwrap(), + groups: GpuRegion::new(0x6000_0000, 0x4000).unwrap(), + submitted: FirmwareVa::new(0xffff_fc21_9000_0000).unwrap(), + capacity: 0x4000, populated: 512, compute_only } + } + fn word(bytes: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) + } + #[test] fn shared_render_pool_never_gets_compute_exclusion() { + for compute in [false, true] { + let mut output = [0xa5; ModernPool::SIZE + 8]; + pool(compute).encode(&mut output).unwrap(); + assert_eq!(word(&output, 0x5c), compute.into()); + assert_eq!(word(&output, 0x48), 8); + assert_eq!(word(&output, 0x84), 1); + assert_eq!(&output[ModernPool::SIZE..], &[0xa5; 8]); + } + } + #[test] fn bad_population_and_insufficient_owners_reject_before_mutation() { + for pages in [1, 511, 513, 0x4000] { + let mut value = pool(true); value.populated = pages; + let mut output = [0xa5; ModernPool::SIZE]; + assert!(value.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; ModernPool::SIZE]); + } + let mut value = pool(true); value.capacity = 0x4001; + let mut output = [0xa5; ModernPool::SIZE]; + assert!(value.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; ModernPool::SIZE]); + } +} + +/// Populate the extended freelist from independently retained 4-MiB owners. +/// Hardware group records describe 2-MiB subregions, not entire owners. +pub(crate) struct ModernBacking<'a>(pub(crate) &'a [GpuRegion]); +impl ModernBacking<'_> { + pub(crate) fn pages(&self) -> Result { + if self.0.is_empty() || self.0.len() > 8 + || self.0.iter().any(|owner| owner.size() != BACKING_SIZE || owner.base() & 0x3fff != 0) { + return Err(Error::Size); + } + Ok(self.0.len() * BACKING_SIZE / PAGE_SIZE) + } + pub(crate) fn page(&self, index: usize) -> Result { + if index >= self.pages()? { return Err(Error::Index); } + let per_owner = BACKING_SIZE / PAGE_SIZE; + Ok(self.0[index / per_owner].base() + (index % per_owner * PAGE_SIZE) as u64) + } + pub(crate) fn group(&self, index: usize) -> Result<[u8; 64], Error> { + if index >= self.pages()? / 512 { return Err(Error::Index); } + let base = self.page(index * 512)?; + let mut record = [0; 64]; + record[..8].copy_from_slice(&(base | (1 << 61)).to_le_bytes()); + Ok(record) + } +} + +#[cfg(test)] +mod backing_tests { + use super::*; + #[test] fn group_records_never_cross_a_noncontiguous_backing_owner() { + let owners = [GpuRegion::new(0x5000_0000, BACKING_SIZE).unwrap(), + GpuRegion::new(0x6000_0000, BACKING_SIZE).unwrap()]; + let backing = ModernBacking(&owners); + assert_eq!(backing.pages(), Ok(2048)); + assert_eq!(backing.page(1024), Ok(0x6000_0000)); + let second = backing.group(2).unwrap(); + assert_eq!(u64::from_le_bytes(second[..8].try_into().unwrap()), (1 << 61) | 0x6000_0000); + assert_eq!(&second[8..], &[0; 56]); + assert!(backing.group(4).is_err()); + assert!(backing.page(2048).is_err()); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_profile.rs b/drivers/gpu/drm/asahi/m3/m3_profile.rs index c70a88029072b6..004ac2c7d1eef2 100644 --- a/drivers/gpu/drm/asahi/m3/m3_profile.rs +++ b/drivers/gpu/drm/asahi/m3/m3_profile.rs @@ -32,6 +32,13 @@ impl Profile { } } + pub(crate) fn gpu_duration_ns(self, elapsed: u64) -> Option { + match self { + Self::Legacy => elapsed.checked_mul(1000).map(|value| value / 24), + Self::Extended => Some(elapsed), + } + } + pub(crate) const fn vm_acknowledged(self, params_bytes: u64, flags: u32) -> bool { // Both profiles consume G15 records; the existing VM padding stays zero. params_bytes >= PARAMS_REQUIRED && flags == 0 @@ -66,4 +73,17 @@ mod tests { assert_eq!(Profile::Extended.user_end(), 0x3ff_ffff_8000); assert_eq!(Profile::Legacy.user_end(), 0x2ff_ffff_8000); } + #[test] + fn native_nanoseconds_are_never_rescaled_as_legacy_ticks() { + assert_eq!( + Profile::Extended.gpu_duration_ns(24_000_000), + Some(24_000_000) + ); + assert_eq!( + Profile::Legacy.gpu_duration_ns(24_000_000), + Some(1_000_000_000) + ); + assert_eq!(Profile::Extended.gpu_duration_ns(u64::MAX), Some(u64::MAX)); + assert_eq!(Profile::Legacy.gpu_duration_ns(u64::MAX), None); + } } diff --git a/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs b/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs index 39cc1f5f2e4908..c3411bfdff8ed2 100644 --- a/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs @@ -113,3 +113,44 @@ pub(crate) fn ring_entry(index: u32, capacity: u32, seeds: &[FirmwareVa]) -> Res if seeds.is_empty() || seeds.len() > capacity as usize || capacity > 65536 || index >= capacity { return Err(Error::Index); } Ok(if (index as usize) < seeds.len() { seeds[index as usize].0.to_le_bytes() } else { [0xcc; 8] }) } + +impl QueueInfo { + pub(crate) const EXTENDED_SIZE: usize = 0x24c0; + + /// Scheduler storage belongs to this same retained allocation; it is not + /// interchangeable with the legacy GPU scratch object. + pub(crate) fn encode_extended(&self, owner: FirmwareVa, output: &mut [u8]) -> Result<(), Error> { + let end = owner.get().checked_add(Self::EXTENDED_SIZE as u64).ok_or(Error::Address)?; + if end > 0xffff_fc30_0000_0000 { return Err(Error::Address); } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + for (offset, value) in [(0, self.state.get()), (8, self.ring.get()), + (16, self.job_list.get()), (24, owner.get() + 0xb0), (0xa4, self.context.get())] { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + for offset in [0x2c, 0x4c] { bytes[offset..offset + 4].fill(0xff); } + for offset in [0x30, 0x34, 0x48] { + bytes[offset..offset + 4].copy_from_slice(&2u32.to_le_bytes()); + } + bytes[0x38..0x40].copy_from_slice(&0xffff_0000_0000_0000u64.to_le_bytes()); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn scheduler_pointer_relocates_with_its_retained_queue_owner() { + let fw = |i: u64| FirmwareVa::new(0xffff_fc21_4000_0000 + i * 0x4000).unwrap(); + let info = QueueInfo { state: fw(1), ring: fw(2), job_list: fw(3), + gpu_scratch: fw(4), context: fw(5), uuid: 9 }; + let mut output = vec![0xa5; QueueInfo::EXTENDED_SIZE + 8]; + info.encode_extended(fw(0), &mut output).unwrap(); + assert_eq!(u64::from_le_bytes(output[24..32].try_into().unwrap()), fw(0).get() + 0xb0); + assert_eq!(&output[0xb0..QueueInfo::EXTENDED_SIZE], &vec![0; QueueInfo::EXTENDED_SIZE - 0xb0]); + assert_eq!(&output[QueueInfo::EXTENDED_SIZE..], &[0xa5; 8]); + let mut short = [0xa5; INFO_SIZE]; + assert!(info.encode_extended(fw(0), &mut short).is_err()); + assert_eq!(short, [0xa5; INFO_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_runtime.rs b/drivers/gpu/drm/asahi/m3/m3_runtime.rs index 1a366d7f4b3407..40399a80a01f39 100644 --- a/drivers/gpu/drm/asahi/m3/m3_runtime.rs +++ b/drivers/gpu/drm/asahi/m3/m3_runtime.rs @@ -1214,6 +1214,9 @@ impl Runtime { } fn boot_inner(&mut self, pdev: &platform::Device>) -> Result { use crate::m3::t8122_start::BootStep; + if self.inner.device.firmware().profile() == super::m3_profile::Profile::Extended { + return self.boot_extended(pdev); + } let root = self.inner.config.root(); dev_info!(pdev.as_ref(), "M3: publishing owned initdata {:#x}\n", root); self.inner.boot_step = BootStep::Publish; @@ -1245,6 +1248,60 @@ impl Runtime { dev_info!(pdev.as_ref(), "M3: firmware accepted owned initdata and device controls\n"); Ok(()) } + fn boot_extended(&mut self, pdev: &platform::Device>) -> Result { + use super::m3_channel_format::Action; + use crate::m3::t8122_start::BootStep; + // The first control is pending when the root becomes visible. Its + // retirement comes from InitData consumption, without a work kick. + let first = self.inner.config.control_extended(Action::Initialize)?; + self.inner.config.publish_io_table(pdev, self.inner.device.firmware())?; + let root = self.inner.config.root(); + Pin::new(&mut self.inner.transport).send_message(0x20, + 0x0081_0000_0000_0000 | (root & ((1u64 << 44) - 1)))?; + self.inner.boot_step = BootStep::AwaitReady; + let deadline = Instant::::now(); + while !self.inner.config.ready()? { + let inner: &mut Inner = &mut *self.inner; + inner.config.drain(&inner.drm)?; + if !inner.state.healthy() { return Err(EIO); } + if deadline.elapsed() >= Delta::from_secs(2) { return Err(ETIMEDOUT); } + fsleep(Delta::from_millis(1)); + } + self.retire_extended_control(first)?; + self.inner.boot_step = BootStep::DeviceControl; + // Each data master registers its two firmware engine classes. + for ordinal in 0..6 { + let action = Action::Register { pipe: ordinal / 2, engine: ordinal % 2 + 1 }; + self.send_extended_control(action)?; + } + self.inner.boot_step = BootStep::PostReady; + self.send_extended_control(Action::Idle { enabled: true })?; + let inner: &mut Inner = &mut *self.inner; + inner.config.check_pstate(&inner.drm, &inner.device, "after native boot")?; + inner.config.log_ready(&inner.drm)?; + inner.state.health.set_gpu_pending(false); + dev_info!(pdev.as_ref(), "M3: extended firmware ready; native controllers and idle policy admitted\n"); + Ok(()) + } + + fn send_extended_control(&mut self, action: super::m3_channel_format::Action) -> Result { + let cursor = self.inner.config.control_extended(action)?; + Pin::new(&mut self.inner.transport).send_message(0x21, 0x0084_0000_0000_0011)?; + self.retire_extended_control(cursor) + } + + fn retire_extended_control(&mut self, expected: u32) -> Result { + let deadline = Instant::::now(); + loop { + let inner: &mut Inner = &mut *self.inner; + inner.config.drain(&inner.drm)?; + if !inner.state.healthy() { return Err(EIO); } + if inner.config.control_done(expected)? { return Ok(()); } + if deadline.elapsed() >= Delta::from_secs(2) { return Err(ETIMEDOUT); } + fsleep(Delta::from_millis(1)); + } + } + fn send_control(&mut self, opcode: u32) -> Result { let next = match self.inner.config.control(opcode) { Ok(next) => next, diff --git a/drivers/gpu/drm/asahi/m3/m3_state_layout.rs b/drivers/gpu/drm/asahi/m3/m3_state_layout.rs index 199b7082b26c29..6eeafea666e742 100644 --- a/drivers/gpu/drm/asahi/m3/m3_state_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_state_layout.rs @@ -66,3 +66,55 @@ pub(crate) fn manager_misc(out: &mut [u8]) -> Result<(), Error> { word(bytes, 0x20, 1); // cpu_flag / reset Ok(()) } + +pub(crate) const EXTENDED_CONTEXT_SIZE: usize = 0x38; +pub(crate) const EXTENDED_NOTIFIER_SIZE: usize = 0x200; + +pub(crate) fn context_extended(output: &mut [u8]) -> Result<(), Error> { + let bytes = output.get_mut(..EXTENDED_CONTEXT_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..2].fill(0xff); + bytes[5] = 1; + bytes[0x26] = 2; + bytes[0x33] = 0xff; + Ok(()) +} + +impl Notifier { + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 { return Err(Error::Address); } + let bytes = output.get_mut(..EXTENDED_NOTIFIER_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..8].copy_from_slice(&self.threshold.get().to_le_bytes()); + word(bytes, 0x10, 0x50); + word(bytes, NOTIFIER_CONTEXT, self.context); + bytes[0xa8..0xb0].fill(0xff); + word(bytes, 0x114, 1); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn registration_state_is_left_to_firmware() { + let mut bytes = [0xa5; EXTENDED_CONTEXT_SIZE + 8]; + context_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[..6], &[255, 255, 0, 0, 0, 1]); + assert_eq!(bytes[0x26], 2); + assert_eq!(bytes[0x33], 255); + assert_eq!(&bytes[EXTENDED_CONTEXT_SIZE..], &[0xa5; 8]); + } + #[test] fn notifier_has_zero_generation_and_distinct_firmware_storage() { + let notifier = Notifier { threshold: FirmwareVa::new(0xffff_fc21_7000_0000).unwrap(), context: 3 }; + let mut bytes = [0xa5; EXTENDED_NOTIFIER_SIZE + 8]; + notifier.encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[8..12], &[0; 4]); + assert_eq!(&bytes[0xa8..0xb0], &[255; 8]); + assert_eq!(&bytes[0x114..0x118], &[1, 0, 0, 0]); + assert_eq!(&bytes[EXTENDED_NOTIFIER_SIZE..], &[0xa5; 8]); + let mut short = [0xa5; NOTIFIER_SIZE]; + assert!(notifier.encode_extended(&mut short).is_err()); + assert_eq!(short, [0xa5; NOTIFIER_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_submit.rs b/drivers/gpu/drm/asahi/m3/m3_submit.rs index 1212113af1618a..c3a25c9a2dc04d 100644 --- a/drivers/gpu/drm/asahi/m3/m3_submit.rs +++ b/drivers/gpu/drm/asahi/m3/m3_submit.rs @@ -177,11 +177,12 @@ pub(crate) struct Queue { shared:Shared,vm:mmu::Vm,usc:u64,fences:FenceContexts, /// Whether the SoC advertises early tiling (`m3_soc::Features::fragment_dependency`). early_tiling:bool, + wide_visibility:bool, urgency:u8, } impl Queue { pub(crate) fn new(shared:Shared,scheduler:Arc>,vm:mmu::Vm,priority:u32,usc:u64, - early_tiling:bool)->Result { + early_tiling:bool, wide_visibility:bool)->Result { agx_uapi::QueueUscWindow{base:usc,user_start:0x4000,user_end:(1u64<<42)-0x8000}.validate().map_err(|_|EINVAL)?; // file.rs passes REALTIME - UAPI priority: 3 is UAPI LOW, 0 is UAPI REALTIME. let urgency=3u8.saturating_sub(priority.min(3) as u8); @@ -189,7 +190,7 @@ impl Queue { 1=>sched::Priority::High,0=>sched::Priority::Kernel,_=>return Err(EINVAL)}; Ok(Self {entity:sched::Entity::new(&scheduler,priority)?,_scheduler:scheduler, shared,vm,usc,fences:FenceContexts::new(1,c_str!("asahi_m3_queue"),kernel::static_lock_class!())?, - early_tiling,urgency}) + early_tiling,wide_visibility,urgency}) } } impl queue::Queue for Queue { @@ -217,7 +218,8 @@ impl queue::Queue for Queue { let agx_uapi::ParsedHardwareCommand::Compute{mut payload,attachments,..}=command else {return Err(ENOTSUPP);}; // Other engines and helpers need their own M3 firmware translation. let wide=kernel::uapi::drm_asahi_compute_flags_DRM_ASAHI_COMPUTE_WIDE_VISIBILITY as u32; - if payload.flags & !wide !=0 || payload.sampler_count!=0 || payload.sampler_heap!=0 { + if payload.flags & !wide !=0 || (!self.wide_visibility && payload.flags & wide != 0) + || payload.sampler_count!=0 || payload.sampler_heap!=0 { pr_err!("M3 compute rejected: flags={:#x} samplers={} heap={:#x} attachments={} timestamps={}/{}\n",payload.flags,payload.sampler_count,payload.sampler_heap,attachments.as_slice().len(),payload.timestamps.start.handle,payload.timestamps.end.handle); return Err(ENOTSUPP); } diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index ed032ab146dd06..faf1f086fd11c3 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -1156,8 +1156,7 @@ impl VmBind { self.1 } - /// Stable wire incarnation for this active root lease. M4 reserves zero as unbound. - #[cfg(CONFIG_DRM_ASAHI_G16)] + /// Stable firmware wire incarnation for this active root lease; zero is unbound. pub(crate) fn generation(&self) -> u8 { self.0.binding.lock().generation } @@ -2670,7 +2669,6 @@ impl Vm { } /// Spend the binding generation only for an incarnation exposed to firmware. - #[cfg(CONFIG_DRM_ASAHI_G16)] pub(crate) fn mark_binding_published(&self) -> Result { let uat = self.gpuvm.lock_inner().uat_inner.clone(); let binding = self.binding.lock(); diff --git a/tools/asahi/m3-runtime-tests.rs b/tools/asahi/m3-runtime-tests.rs index abac06668c0a1c..458603366c954b 100644 --- a/tools/asahi/m3-runtime-tests.rs +++ b/tools/asahi/m3-runtime-tests.rs @@ -36,3 +36,13 @@ mod m3_compute_layout; #[path = "../../drivers/gpu/drm/asahi/m3/m3_channel_format.rs"] mod m3_channel_format; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs"] +mod m3_compute_sequence; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_state_layout.rs"] +mod m3_state_layout; + + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_compute_storage.rs"] +mod m3_compute_storage; From c0184b54c40c263cd96e9dd06075e1ec4c20745b Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:40:41 -0400 Subject: [PATCH 07/15] drm/asahi: dispatch G15 compute through extended T8122 firmware Dispatch extended firmware compute records through the existing M3 runtime while preserving G15 CDM streams. Retain owned pool backing and leave firmware-managed register setup to the extended interface. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/m3/m3_compute.rs | 162 +++++++++++++++++-------- drivers/gpu/drm/asahi/m3/m3_config.rs | 10 ++ drivers/gpu/drm/asahi/m3/m3_device.rs | 2 +- drivers/gpu/drm/asahi/m3/m3_runtime.rs | 18 ++- 4 files changed, 132 insertions(+), 60 deletions(-) diff --git a/drivers/gpu/drm/asahi/m3/m3_compute.rs b/drivers/gpu/drm/asahi/m3/m3_compute.rs index 34aa0c0d2cd3fb..577510b7e9ecf6 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute.rs @@ -40,25 +40,28 @@ pub(crate) struct Compute { barriers: Option, /// The register lists of the SoC's GPU (`m3_soc::Soc::registers`). registers: command::RegisterSet, + profile: super::m3_profile::Profile, /// Last: drops after the command aliases. _lower_tlbi: crate::m3::m3_memory::LowerTlbi, } impl Compute { pub(crate) fn new(dev: &driver::AsahiDrmDevice, uat: &mmu::Uat, vm: &mmu::Vm, stats: Region, control: crate::m3::agx_compute::Control, - registers: command::RegisterSet) -> Result { + registers: command::RegisterSet, profile: super::m3_profile::Profile) -> Result { let lower_tlbi=crate::m3::m3_memory::LowerTlbi; let binding=uat.bind(vm)?; let mut objects=KVec::new(); let mut client_command=None; let mut command_bytes = KVec::new(); - command_bytes.resize(command::SIZE, 0u8, GFP_KERNEL)?; + command_bytes.resize(if profile == super::m3_profile::Profile::Extended { + command::EXTENDED_SIZE + } else { command::SIZE }, 0u8, GFP_KERNEL)?; let mut sequence_bytes = KVec::new(); sequence_bytes.resize(microsequence::STORAGE, 0u8, GFP_KERNEL)?; // A private compute aperture, disjoint from the retained render graph. // Address-bearing graph fields are constructed from their live owners. for index in 0..storage::COUNT { - let a=storage::batch_allocation(index).map_err(|_|EINVAL)?; + let a=storage::batch_allocation_for(profile,index).map_err(|_|EINVAL)?; let mut b=match a.mapping { Mapping::CommandAlias(va)=>Buffer::at(dev,uat.kernel_vm(),Some((uat.kernel_lower_vm(),va)),None,a.size,true)?, Mapping::Client(va)=>Buffer::at(dev,uat.kernel_vm(),Some((vm,va)),None,a.size,true)?, @@ -68,10 +71,13 @@ impl Compute { if matches!(a.mapping,Mapping::CommandAlias(_)) {client_command=Some(b.map_gpu_view(vm)?);} objects.push(b,GFP_KERNEL)?; } - Self::encode_command(&mut objects, &mut command_bytes, binding.slot(), control, 1, 0, registers)?; - Self::encode_sequence(&mut objects, &mut sequence_bytes, stats, binding.slot(), timeline::stamp(0xc1000000,1), 0)?; - UMA_POOL.initialize(&mut objects)?; - Self::initialize_state(&mut objects, binding.slot())?; + Self::encode_command(&mut objects, &mut command_bytes, binding.slot(), control, 1, 0, + registers, profile, binding.generation())?; + Self::encode_sequence(&mut objects, &mut sequence_bytes, stats, binding.slot(), timeline::stamp(0xc1000000,1), 0, profile, 0)?; + if profile == super::m3_profile::Profile::Extended { + UMA_POOL.initialize_extended(&mut objects, 1, true)?; + } else { UMA_POOL.initialize(&mut objects)?; } + Self::initialize_state(&mut objects, binding.slot(), profile)?; // Queue references are firmware VAs from live allocations. In contrast // to render's unpublished seed entries, compute dispatches entry zero // immediately, so its initial producer cursor remains one. @@ -80,8 +86,13 @@ impl Compute { job_list: address(JOB_LIST)?, gpu_scratch: address(QUEUE_SCRATCH)?, context: address(GPU_CONTEXT)?, uuid: 0xc10000 }; let command = address(storage::COMMAND)?; - let mut bytes = [0; queue::INFO_SIZE]; - info.encode(&mut bytes).map_err(|_| EINVAL)?; + let mut bytes = KVec::new(); + bytes.resize(if profile == super::m3_profile::Profile::Extended { + queue::QueueInfo::EXTENDED_SIZE + } else { queue::INFO_SIZE }, 0u8, GFP_KERNEL)?; + if profile == super::m3_profile::Profile::Extended { + info.encode_extended(address(storage::QUEUE)?, &mut bytes).map_err(|_| EINVAL)?; + } else { info.encode(&mut bytes).map_err(|_| EINVAL)?; } objects[storage::QUEUE].write(0, &bytes)?; let mut state = [0; queue::STATE_SIZE]; queue::RingState { capacity: queue::QUALIFIED_CAPACITY, done: 0, read: 0, write: 1 } @@ -107,54 +118,74 @@ impl Compute { // the invalidation. crate::mem::sync();crate::mem::tlbi_all();crate::mem::sync(); unsafe {core::arch::asm!("isb",options(nostack,preserves_flags))}; - Ok(Self {_client_command:client_command.ok_or(EINVAL)?,objects,command_bytes,sequence_bytes,_binding:binding,stats,last_progress_ns:0,sequence:1,head:1,cached_views:KVec::new(),slot:0,batch_count:1,checkpoint:(0,0,0),barriers:None,registers,_lower_tlbi:lower_tlbi}) + Ok(Self {_client_command:client_command.ok_or(EINVAL)?,objects,command_bytes,sequence_bytes,_binding:binding,stats,last_progress_ns:0,sequence:1,head:1,cached_views:KVec::new(),slot:0,batch_count:1,checkpoint:(0,0,0),barriers:None,registers,profile,_lower_tlbi:lower_tlbi}) } fn encode_command(objects: &mut [Buffer], bytes: &mut [u8], context: u32, control: crate::m3::agx_compute::Control, sequence: u64, slot: usize, - register_set: command::RegisterSet) -> Result { - let offset = |owner:usize| storage::slot_offset(owner,slot).map_err(|_|EINVAL); + register_set: command::RegisterSet, profile: super::m3_profile::Profile, generation: u8) -> Result { + let offset = |owner:usize| storage::slot_offset_for(profile,owner,slot).map_err(|_|EINVAL); let fw = |owner: usize| queue::FirmwareVa::new(objects[owner].va()+offset(owner)? as u64).map_err(|_| EINVAL); let preemption = &objects[PREEMPTION]; let value = command::Command { context, counter: timeline::previous(sequence), notifier: fw(NOTIFIER)?, - preemption: GpuRegion::new(preemption.gpu_va()?+offset(PREEMPTION)? as u64, storage::allocation(PREEMPTION).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL)?, + preemption: GpuRegion::new(preemption.gpu_va()?+offset(PREEMPTION)? as u64, storage::allocation_for(profile,PREEMPTION).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL)?, cdm: command::GpuVa::new(control.base).map_err(|_| EINVAL)?, cdm_last: command::GpuVa::new(control.end.checked_sub(4).ok_or(EINVAL)?).map_err(|_| EINVAL)?, gpu_alias: command::GpuVa::new(objects[storage::COMMAND].gpu_va()?+offset(storage::COMMAND)? as u64).map_err(|_| EINVAL)?, - microsequence: fw(storage::MICROSEQUENCE)?, microsequence_size: microsequence::LENGTH as u32, + microsequence: fw(storage::MICROSEQUENCE)?, microsequence_size: + if profile == super::m3_profile::Profile::Extended { microsequence::Sequence::EXTENDED_LENGTH as u32 } + else { microsequence::LENGTH as u32 }, stamp: fw(storage::STAMP)?, fw_stamp: fw(storage::FW_STAMP)?, stamp_value: timeline::stamp(0xc1000000,sequence), timestamps: [fw(TIMESTAMPS[0])?, fw(TIMESTAMPS[1])?], pool: fw(storage::POOL)?, tail_scratch: fw(TAIL_SCRATCH)?, flush_stamps: false, register_set, }; - value.encode(bytes).map_err(|_| EINVAL)?; + value.encode_with_usc(profile, generation, 0, + if profile == super::m3_profile::Profile::Extended { control.usc_base } else { 0 }, bytes).map_err(|_| EINVAL)?; objects[storage::COMMAND].write(offset(storage::COMMAND)?, bytes) } fn encode_sequence(objects: &mut [Buffer], bytes: &mut [u8], stats: Region, - context: u32, stamp_value: u32, slot: usize) -> Result { - let offset = |owner:usize| storage::slot_offset(owner,slot).map_err(|_|EINVAL); + context: u32, stamp_value: u32, slot: usize, profile: super::m3_profile::Profile, + queue_sequence: u64) -> Result { + let offset = |owner:usize| storage::slot_offset_for(profile,owner,slot).map_err(|_|EINVAL); let fw = |owner: usize| queue::FirmwareVa::new(objects[owner].va()+offset(owner)? as u64).map_err(|_| EINVAL); - let region = |owner: usize| Region::new(objects[owner].va()+offset(owner)? as u64, storage::allocation(owner).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL); + let region = |owner: usize| Region::new(objects[owner].va()+offset(owner)? as u64, storage::allocation_for(profile,owner).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL); let value = microsequence::Sequence { command: region(storage::COMMAND)?, stats, notifier: region(NOTIFIER)?, - queue: fw(storage::QUEUE)?, context, generation: 1, pool: fw(storage::POOL)?, + queue: fw(storage::QUEUE)?, context, + generation: if profile == super::m3_profile::Profile::Extended { 0 } else { 1 }, pool: fw(storage::POOL)?, scratch: [fw(MICROSEQUENCE_SCRATCH[0])?, fw(MICROSEQUENCE_SCRATCH[1])?, fw(MICROSEQUENCE_SCRATCH[2])?, fw(MICROSEQUENCE_SCRATCH[3])?, fw(MICROSEQUENCE_SCRATCH[4])?], fw_stamp: fw(storage::FW_STAMP)?, stamp_value, }; - value.encode(bytes).map_err(|_| EINVAL)?; + if profile == super::m3_profile::Profile::Extended { + let auxiliary = command::GpuVa::new(objects[TAIL_SCRATCH].gpu_va()? + offset(TAIL_SCRATCH)? as u64).map_err(|_| EINVAL)?; + let context_store = GpuRegion::new(objects[MICROSEQUENCE_SCRATCH[1]].gpu_va()? + + offset(MICROSEQUENCE_SCRATCH[1])? as u64, 0x40000).map_err(|_| EINVAL)?; + value.encode_extended(auxiliary, context_store, queue_sequence, bytes).map_err(|_| EINVAL)?; + } else { value.encode(bytes).map_err(|_| EINVAL)?; } objects[storage::MICROSEQUENCE].write(offset(storage::MICROSEQUENCE)?, bytes) } - fn initialize_state(objects: &mut [Buffer], context_slot: u32) -> Result { + fn initialize_state(objects: &mut [Buffer], context_slot: u32, profile: super::m3_profile::Profile) -> Result { let address = |owner: usize| queue::FirmwareVa::new(objects[owner].va()).map_err(|_| EINVAL); let notification = state::Notifier { threshold: address(storage::EVENT)?, context: context_slot }; let list_owner = address(JOB_LIST)?; let mut context = [0; state::CONTEXT_SIZE]; - state::context(&mut context).map_err(|_| EINVAL)?; - objects[GPU_CONTEXT].write(0, &context)?; - let mut notifier = [0; state::NOTIFIER_SIZE]; - notification.encode(&mut notifier).map_err(|_| EINVAL)?; + if profile == super::m3_profile::Profile::Extended { + state::context_extended(&mut context).map_err(|_| EINVAL)?; + } else { state::context(&mut context).map_err(|_| EINVAL)?; } + let context_size = if profile == super::m3_profile::Profile::Extended { + state::EXTENDED_CONTEXT_SIZE + } else { state::CONTEXT_SIZE }; + objects[GPU_CONTEXT].write(0, &context[..context_size])?; + let mut notifier = KVec::new(); + notifier.resize(if profile == super::m3_profile::Profile::Extended { + state::EXTENDED_NOTIFIER_SIZE + } else { state::NOTIFIER_SIZE }, 0u8, GFP_KERNEL)?; + if profile == super::m3_profile::Profile::Extended { + notification.encode_extended(&mut notifier).map_err(|_| EINVAL)?; + } else { notification.encode(&mut notifier).map_err(|_| EINVAL)?; } objects[NOTIFIER].write(0, ¬ifier)?; let mut list = [0; state::JOB_LIST_SIZE]; state::job_list(&mut list, list_owner).map_err(|_| EINVAL)?; @@ -182,7 +213,7 @@ impl Compute { let binding = uat.bind(vm)?; let mut views = KVec::new(); for i in 0..storage::COUNT { - if matches!(storage::allocation(i).map_err(|_|EINVAL)?.mapping,Mapping::Client(_)) { + if matches!(storage::allocation_for(self.profile,i).map_err(|_|EINVAL)?.mapping,Mapping::Client(_)) { views.push((i,self.objects[i].map_gpu_view(vm)?),GFP_KERNEL)?; } } @@ -221,18 +252,17 @@ impl Compute { if self.batch_count>=storage::SLOTS {return Err(E2BIG);} self.slot=self.batch_count; for i in storage::RESET { - let offset=storage::slot_offset(i,self.slot).map_err(|_|EINVAL)?; - let size=storage::allocation(i).map_err(|_|EINVAL)?.size; + let offset=storage::slot_offset_for(self.profile,i,self.slot).map_err(|_|EINVAL)?; + let size=storage::allocation_for(self.profile,i).map_err(|_|EINVAL)?.size; self.objects[i].fill_range(offset,size,if i==PREEMPTION {0xcc} else {0})?; } self.objects[NOTIFIER].u32(state::NOTIFIER_CONTEXT,self._binding.slot())?; self.sequence=self.sequence.checked_add(1).ok_or(EOVERFLOW)?; - Self::encode_command(&mut self.objects,&mut self.command_bytes,self._binding.slot(),control,self.sequence,self.slot,self.registers)?; + Self::encode_command(&mut self.objects,&mut self.command_bytes,self._binding.slot(),control,self.sequence,self.slot,self.registers,self.profile,self._binding.generation())?; let stamp=self.stamp(); - Self::encode_sequence(&mut self.objects,&mut self.sequence_bytes,self.stats,self._binding.slot(),stamp,self.slot)?; - self.objects[storage::EVENT].u32(0,self.sequence as u32)?; - self.objects[storage::COUNTER].u32(0,self.sequence as u32)?; - let address=self.objects[storage::COMMAND].va()+storage::slot_offset(storage::COMMAND,self.slot).map_err(|_|EINVAL)? as u64; + Self::encode_sequence(&mut self.objects,&mut self.sequence_bytes,self.stats,self._binding.slot(),stamp,self.slot,self.profile,self.sequence - 1)?; + self.write_counters()?; + let address=self.objects[storage::COMMAND].va()+storage::slot_offset_for(self.profile,storage::COMMAND,self.slot).map_err(|_|EINVAL)? as u64; self.objects[storage::RING].u64(usize::from(self.head)*8,address)?; self.head=(self.head+1)%queue::QUALIFIED_CAPACITY as u16; self.batch_count+=1; @@ -245,39 +275,55 @@ impl Compute { pub(crate) fn abort_batch(&mut self)->Result { (self.sequence,self.head,self.batch_count)=self.checkpoint; self.slot=self.batch_count.saturating_sub(1); - self.objects[storage::EVENT].u32(0,self.sequence as u32)?; - self.objects[storage::COUNTER].u32(0,self.sequence as u32)?; + self.write_counters()?; self.objects[storage::QUEUE_STATE].u32(queue::WRITE,u32::from(self.head))?; agx_memory::publish();Ok(()) } + fn write_counters(&mut self) -> Result { + for index in [storage::EVENT, storage::COUNTER] { + if self.profile == super::m3_profile::Profile::Extended { + self.objects[index].u64(0, self.sequence)?; + } else { self.objects[index].u32(0, self.sequence as u32)?; } + } + Ok(()) + } + fn offset(&self,owner:usize)->Result { - storage::slot_offset(owner,self.slot).map_err(|_|EINVAL) + storage::slot_offset_for(self.profile,owner,self.slot).map_err(|_|EINVAL) } /// Same cache-generation contract as M4: StartCompute and the persistent /// notifier must use the same tag before the firmware flushes user stamps. /// RTKit 2419: StartCompute.counter1 +0x34; JobMeta.flush_stamps +0x804. pub(crate) fn prepare_completion(&mut self,coalesce:bool)->Result { + if coalesce && self.profile == super::m3_profile::Profile::Extended { return Err(EINVAL); } let sequence=self.offset(storage::MICROSEQUENCE)?; let command=self.offset(storage::COMMAND)?; self.objects[storage::MICROSEQUENCE].u32(sequence+microsequence::GENERATION,0)?; - self.objects[storage::COMMAND].u32(command+command::FLUSH_STAMPS,u32::from(!coalesce))?; + self.objects[storage::COMMAND].u32(command+if self.profile == super::m3_profile::Profile::Extended { 0x81c } else { command::FLUSH_STAMPS },u32::from(!coalesce))?; agx_memory::publish();Ok(()) } pub(crate) fn set_user_timestamps(&mut self,addresses:[u64;2])->Result { - let offset=self.offset(storage::COMMAND)?+command::USER_TIMESTAMPS; + let offset=self.offset(storage::COMMAND)?+if self.profile == super::m3_profile::Profile::Extended { + 0x844 + } else { command::USER_TIMESTAMPS }; self.objects[storage::COMMAND].u64(offset,addresses[0])?; self.objects[storage::COMMAND].u64(offset+8,addresses[1])?; let pointer=if addresses==[0,0] {0} else {self.objects[storage::COMMAND].va()+offset as u64}; let sequence=self.offset(storage::MICROSEQUENCE)?; - for start in [microsequence::TIMESTAMP_START,microsequence::TIMESTAMP_END] { + for start in if self.profile == super::m3_profile::Profile::Extended { [0x1bc, 0x20c] } + else { [microsequence::TIMESTAMP_START,microsequence::TIMESTAMP_END] } { self.objects[storage::MICROSEQUENCE].u64(sequence+start+microsequence::USER_TIMESTAMP_POINTER,pointer)?; } agx_memory::publish();Ok(()) } pub(crate) fn set_attachments(&mut self,attachments:&crate::m3::agx_attachments::Attachments)->Result { let sequence=self.offset(storage::MICROSEQUENCE)?; - self.objects[storage::MICROSEQUENCE].write(sequence+microsequence::ATTACHMENTS,&attachments.0)?; - self.objects[storage::MICROSEQUENCE].write(sequence+microsequence::HAS_ATTACHMENTS,&[u8::from(attachments.count()!=0)])?; + self.objects[storage::MICROSEQUENCE].write(sequence+if self.profile == super::m3_profile::Profile::Extended { + microsequence::Sequence::EXTENDED_ATTACHMENTS + } else { microsequence::ATTACHMENTS },&attachments.0)?; + self.objects[storage::MICROSEQUENCE].write(sequence+if self.profile == super::m3_profile::Profile::Extended { + microsequence::Sequence::EXTENDED_HAS_ATTACHMENTS + } else { microsequence::HAS_ATTACHMENTS },&[u8::from(attachments.count()!=0)])?; agx_memory::publish();Ok(()) } fn stamp(&self) -> u32 { timeline::stamp(0xc1000000,self.sequence) } @@ -319,26 +365,31 @@ impl Compute { agx_memory::publish();crate::mem::sync();Ok(()) } pub(crate) fn gpu_ns(&mut self)->Result { - let mut total=0; - for slot in 0..self.batch_count {total+=self.slot_gpu_ns(slot)?;} + let mut total: u64 = 0; + for slot in 0..self.batch_count { + total = total.checked_add(self.slot_gpu_ns(slot)?).ok_or(EOVERFLOW)?; + } Ok(total) } pub(crate) fn slot_gpu_ns(&mut self,slot:usize)->Result { if slot>=self.batch_count {return Err(EINVAL);} - let start=self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset(TIMESTAMPS[0],slot).map_err(|_|EINVAL)?)?; - let end=self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset(TIMESTAMPS[1],slot).map_err(|_|EINVAL)?)?; - Ok(if start!=0 && end>=start {(end-start)*1000/24} else {0}) + let start=self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],slot).map_err(|_|EINVAL)?)?; + let end=self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],slot).map_err(|_|EINVAL)?)?; + if start == 0 || end < start { return Ok(0); } + self.profile.gpu_duration_ns(end - start).ok_or(EOVERFLOW) } /// A batch span includes gaps between its compute commands, unlike gpu_ns. /// Read only after stamps, queues, events and idle state prove retirement. pub(crate) fn batch_gpu_span(&mut self)->Result<[u64;2]> { if self.batch_count == 0 {return Err(EINVAL);} - Ok([self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset(TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?, - self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset(TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?]) + Ok([self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?, + self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?]) } pub(crate) fn head(&self) -> u16 { self.head } pub(crate) fn ordinal(&self)->u64 {self.sequence} - pub(crate) fn first(&self) -> bool { self.sequence == self.batch_count as u64 } + pub(crate) fn first(&self) -> bool { + self.profile == super::m3_profile::Profile::Legacy && self.sequence == self.batch_count as u64 + } pub(crate) fn progress(&mut self, dev: &driver::AsahiDrmDevice) -> Result { // Report real completions by elapsed time as well as sequence count; // batches need not land on a multiple of 128. @@ -361,10 +412,15 @@ impl Compute { pub(crate) fn queue(&self)->u64 {self.objects[storage::QUEUE].va()} pub(crate) fn complete(&mut self)->Result { - Ok(self.objects[storage::STAMP].read_u32(0)?==self.stamp() - && self.objects[storage::FW_STAMP].read_u32(0)?==self.stamp() - && self.objects[storage::QUEUE_STATE].read_u32(queue::DONE)?==u32::from(self.head) - && self.objects[storage::QUEUE_STATE].read_u32(queue::READ)?==u32::from(self.head)) + let stamped = self.objects[storage::STAMP].read_u32(0)? == self.stamp() + && self.objects[storage::FW_STAMP].read_u32(0)? == self.stamp() + && self.objects[storage::QUEUE_STATE].read_u32(queue::DONE)? == u32::from(self.head) + && self.objects[storage::QUEUE_STATE].read_u32(queue::READ)? == u32::from(self.head); + if !stamped { return Ok(false); } + if self.profile == super::m3_profile::Profile::Extended { + return Ok(self.objects[storage::POOL].read_u64(0x54)? == self.sequence); + } + Ok(true) } pub(crate) fn log(&mut self,dev:&driver::AsahiDrmDevice)->Result { for off in (0..512).step_by(128) { diff --git a/drivers/gpu/drm/asahi/m3/m3_config.rs b/drivers/gpu/drm/asahi/m3/m3_config.rs index bfb408b07a5307..ffd1bb963b9055 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -177,6 +177,16 @@ impl Config { Ok(()) } + pub(crate) fn global_table_views(&mut self, vm: &mmu::Vm) -> Result> { + let mut mappings = KVec::new(); + if self.profile == super::m3_profile::Profile::Extended { + for index in [super::m3_init_26::GLOBAL_POOL, super::m3_init_26::GLOBAL_PARAMETERS] { + mappings.push(self.objects[index].map_gpu_view(vm)?, GFP_KERNEL)?; + } + } + Ok(mappings) + } + pub(crate) fn root(&self) -> u64 { self.objects[storage::ROOT].va() } pub(crate) fn log_globals_debug(&mut self,dev:&driver::AsahiDrmDevice)->Result { let value=self.objects[GLOBALS].read_u32(crate::m3::m3_globals_debug::OFFSET)?; diff --git a/drivers/gpu/drm/asahi/m3/m3_device.rs b/drivers/gpu/drm/asahi/m3/m3_device.rs index 05a3961369a6d2..474a8b3f561ec7 100644 --- a/drivers/gpu/drm/asahi/m3/m3_device.rs +++ b/drivers/gpu/drm/asahi/m3/m3_device.rs @@ -129,6 +129,7 @@ impl Device { } e.sgx_setup() } + None if device.firmware.profile() == super::m3_profile::Profile::Extended => None, None => Some(soc.sgx_setup.ok_or(ENODEV)?), }; if let Some((setup_offset, setup_value)) = setup { @@ -179,7 +180,6 @@ impl Device { if self.firmware.profile() == super::m3_profile::Profile::Extended { // Adaptive clock gating can leave individual engine banks inaccessible // even while the GPU performance-state register says powered up. - dev_info!(self.dev.as_ref(), "M3: adaptive firmware fault evidence uses retained metadata; engine banks may be gated\n"); return Ok(()); } let sgx_guard = self.sgx.try_access().ok_or(ENODEV)?; diff --git a/drivers/gpu/drm/asahi/m3/m3_runtime.rs b/drivers/gpu/drm/asahi/m3/m3_runtime.rs index 40399a80a01f39..058c6610399ae3 100644 --- a/drivers/gpu/drm/asahi/m3/m3_runtime.rs +++ b/drivers/gpu/drm/asahi/m3/m3_runtime.rs @@ -389,7 +389,7 @@ impl Inner { let index=found.unwrap_or(self.jobs.len()); if found.is_none() { let job=match control { - crate::m3::m3_submit::Command::Compute(c)=>NativeJob::Compute(crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,c,self.device.soc().registers).map_err(early)?), + crate::m3::m3_submit::Command::Compute(c)=>NativeJob::Compute(crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,c,self.device.soc().registers,self.device.firmware().profile()).map_err(early)?), crate::m3::m3_submit::Command::Render{..}=>NativeJob::Render(crate::m3::m3_render::Render::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,self.device.soc().clusters,self.device.soc().registers).map_err(early)?), }; self.jobs.push(job,GFP_KERNEL).map_err(|_|early(ENOMEM))?; @@ -469,6 +469,9 @@ impl Inner { crate::mem::sync(); self.t8122_verdict=false; let previous_events=self.config.completed_events; + if self.device.firmware().profile() == super::m3_profile::Profile::Extended { + packet.vm.mark_binding_published().map_err(early)?; + } let publication_tick=if crate::debug::debug_enabled(crate::debug::DebugFlags::M3PassTiming) {physical_counter()} else {0}; self.state.health.set_gpu_pending(true); self.gpu_pending = true; @@ -535,7 +538,7 @@ impl Inner { let control=first_compute.ok_or(EINVAL)?; if existing.is_none() { let job=crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm, - self.config.stats_region()?,control,self.device.soc().registers)?; + self.config.stats_region()?,control,self.device.soc().registers,self.device.firmware().profile())?; self.jobs.push(NativeJob::Compute(job),GFP_KERNEL)?; } else { let NativeJob::Compute(j)=&mut self.jobs[compute_index] else {return Err(EINVAL);}; @@ -1059,7 +1062,7 @@ impl Runtime { // TTBAT of the admitted reservations, the firmware kernel root (`pagetables`) as // every context's upper half, and the host window (root entry 2) through the // shared L2. The RTKit is awake and no initdata or GPU job has been published. - let (uat,window)=adopted.insert(adopt_uat(&drm,device.soc(),&device.firmware().resources)?); + let (uat,window)=adopted.insert(adopt_uat(&drm,device.soc(),&device.firmware().resources,device.firmware().profile())?); let config=Config::new(&drm,uat,device.firmware(),&contents)?; // Link every kernel table the configuration created before the firmware sees an // address in it, then drop any translation cached before they existed. @@ -1103,7 +1106,8 @@ impl Runtime { // (7.1.12's M3 VM job lifetime, the shared `mmu::lifetime` on 7.2). let vm=self.inner.uat.new_vm(id, range)?.with_tracked_status()?; let mut reserved=KVec::new();reserved.push(reserved_range,GFP_KERNEL)?;reserved.push(0x100_8000_0000..0x101_0000_0000,GFP_KERNEL)?;reserved.push(0x10_0000_0000..0x10_0800_0000,GFP_KERNEL)?;reserved.push(0x10_7400_0000..0x10_7402_8000,GFP_KERNEL)?; - vm.install_driver_mappings(KVec::new(),reserved)?; + let mappings = self.inner.config.global_table_views(&vm)?; + vm.install_driver_mappings(mappings,reserved)?; Ok(vm) } pub(crate) fn map_timestamp(&mut self,mut bo:crate::gem::ObjectRef,range:core::ops::Range)->Result { @@ -1369,13 +1373,15 @@ pub(crate) fn physical_counter() -> u64 { /// (ttbs, pagetables, handoff, shared-l2) and the PPL exchange with its startup epoch, as the /// G15 backend does it on 7.2 (`g15::uat`). fn adopt_uat(drm: &driver::AsahiDrmDevice, soc: &'static crate::m3::m3_soc::Soc, - resources: &crate::m3::agx_resources::Resources) -> Result<(mmu::Uat, crate::g15::uat::Window)> { + resources: &crate::m3::agx_resources::Resources, profile: super::m3_profile::Profile) + -> Result<(mmu::Uat, crate::g15::uat::Window)> { let cfg = soc.hwcfg.ok_or(ENODEV)?; let [ttbs, tables, handoff, shared_l2, ..] = resources.regions; let range = |r: crate::m3::agx_resources::Region| r.base..r.base + r.size; let geometry = mmu::UatGeometry { ias: cfg.uat_ias.into(), oas: cfg.uat_oas }; let regions = mmu::AdoptedRegions { handoff: range(handoff), ttbs: range(ttbs), kernel_root: tables.base }; - let uat = mmu::Uat::new_adopted(drm, geometry, ®ions)?; + let uat = mmu::Uat::new_adopted_with_user_upper(drm, geometry, ®ions, + profile == super::m3_profile::Profile::Legacy)?; let mut window = crate::g15::uat::Window::from_regions(handoff.base, ttbs.base, tables.base, shared_l2.base)?; window.bootstrap(drm.as_ref().as_ref(), &uat)?; // 7.1.12 cleaned every reserved-table write to the point of coherency; these regions may be From 99b74c9a52496fe6fcf5a47442093483bbd8fcbf Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Fri, 30 Oct 2026 21:40:42 -0400 Subject: [PATCH 08/15] drm/asahi: enable J613 extended firmware render and retirement Encode extended tiler and fragment work, scene records, independent notifications and attachment visibility through the existing runtime. Retire native pools and enable only the exact J613 26.6.2 profile. Keep the public UAPI and physical G15 stream formats unchanged. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/m3/m3_board.rs | 2 +- drivers/gpu/drm/asahi/m3/m3_compute.rs | 10 +- drivers/gpu/drm/asahi/m3/m3_config.rs | 12 + drivers/gpu/drm/asahi/m3/m3_drm.rs | 10 +- drivers/gpu/drm/asahi/m3/m3_firmware.rs | 10 +- .../gpu/drm/asahi/m3/m3_fragment_command.rs | 138 ++++++++++-- .../gpu/drm/asahi/m3/m3_parameter_layout.rs | 74 +++++++ drivers/gpu/drm/asahi/m3/m3_pass.rs | 4 +- drivers/gpu/drm/asahi/m3/m3_pass_layout.rs | 36 +++ drivers/gpu/drm/asahi/m3/m3_profile.rs | 21 ++ drivers/gpu/drm/asahi/m3/m3_render.rs | 206 +++++++++++++----- .../gpu/drm/asahi/m3/m3_render_sequence.rs | 168 ++++++++++++++ drivers/gpu/drm/asahi/m3/m3_runtime.rs | 4 +- drivers/gpu/drm/asahi/m3/m3_scene_layout.rs | 40 ++++ drivers/gpu/drm/asahi/m3/m3_shared_layout.rs | 23 ++ drivers/gpu/drm/asahi/m3/m3_submit.rs | 7 +- drivers/gpu/drm/asahi/m3/m3_sync_layout.rs | 55 +++++ drivers/gpu/drm/asahi/m3/m3_tiler_command.rs | 131 +++++++++-- drivers/gpu/drm/asahi/m3/t8122_admission.rs | 40 +++- tools/asahi/m3-runtime-tests.rs | 36 +++ 20 files changed, 926 insertions(+), 101 deletions(-) diff --git a/drivers/gpu/drm/asahi/m3/m3_board.rs b/drivers/gpu/drm/asahi/m3/m3_board.rs index c44b2a42d99d55..5d4ea4277b6181 100644 --- a/drivers/gpu/drm/asahi/m3/m3_board.rs +++ b/drivers/gpu/drm/asahi/m3/m3_board.rs @@ -13,7 +13,7 @@ use crate::m3::m3_resources::{ use crate::m3::m3_soc::Soc; /// Whether the root node's compatible list contains `compatible`. -fn board_is(compatible: &[u8]) -> bool { +pub(crate) fn board_is(compatible: &[u8]) -> bool { let Some(root) = kernel::of::root() else { return false; }; diff --git a/drivers/gpu/drm/asahi/m3/m3_compute.rs b/drivers/gpu/drm/asahi/m3/m3_compute.rs index 577510b7e9ecf6..b6e9444ed54499 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute.rs @@ -350,10 +350,12 @@ impl Compute { let barriers=self.barriers.as_mut().ok_or(EINVAL)?; let offset=self.slot*64; let mut bytes=[0;crate::m3::m3_sync_layout::BARRIER_SIZE]; - crate::m3::m3_sync_layout::Barrier { + let barrier=crate::m3::m3_sync_layout::Barrier { stamp:dependency.stamp,wait_value:dependency.value,event:dependency.event, self_value:value,uuid:0xc10000,dependency:crate::m3::m3_sync_layout::Dependency::PreviousPass, - }.encode(&mut bytes).map_err(|_|EINVAL)?; + }; + if self.profile==super::m3_profile::Profile::Extended {barrier.encode_extended(&mut bytes)} + else {barrier.encode(&mut bytes)}.map_err(|_|EINVAL)?; barriers.write(offset,&bytes)?; let address=barriers.va()+offset as u64; let (slots,next)=queue::insert_before_last(self.head,queue::QUALIFIED_CAPACITY).map_err(|_|EINVAL)?; @@ -382,8 +384,8 @@ impl Compute { /// Read only after stamps, queues, events and idle state prove retirement. pub(crate) fn batch_gpu_span(&mut self)->Result<[u64;2]> { if self.batch_count == 0 {return Err(EINVAL);} - Ok([self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?, - self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?]) + Ok([self.profile.profiler_ticks(self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?), + self.profile.profiler_ticks(self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?)]) } pub(crate) fn head(&self) -> u16 { self.head } pub(crate) fn ordinal(&self)->u64 {self.sequence} diff --git a/drivers/gpu/drm/asahi/m3/m3_config.rs b/drivers/gpu/drm/asahi/m3/m3_config.rs index ffd1bb963b9055..02b847e210dc9b 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -246,6 +246,18 @@ impl Config { self.thermal.busy(now); Ok(()) } + pub(crate) fn render_stats(&self) -> Result<[init::Region;2]> { + if self.profile==super::m3_profile::Profile::Legacy { + let value=self.stats_region()?;return Ok([value;2]); + } + let region=|index:usize,offset:usize| { + let object=&self.objects[index]; + init::Region::new(object.va().checked_add(offset as u64).ok_or(EOVERFLOW)?, + object.size().checked_sub(offset).ok_or(EINVAL)?).map_err(|_|EINVAL) + }; + Ok([region(UNKNOWN_C0,4)?,region(UNKNOWN_C1,8)?]) + } + pub(crate) fn stats_region(&self) -> Result { let owner = &self.objects[if self.profile == super::m3_profile::Profile::Extended { UNKNOWN_C3 diff --git a/drivers/gpu/drm/asahi/m3/m3_drm.rs b/drivers/gpu/drm/asahi/m3/m3_drm.rs index 4aa982ff95a5cf..922d76ba2055bc 100644 --- a/drivers/gpu/drm/asahi/m3/m3_drm.rs +++ b/drivers/gpu/drm/asahi/m3/m3_drm.rs @@ -113,10 +113,12 @@ impl Backend { crate::m3::t8122_start::not_admitted(pdev.as_ref(), soc); })?; dev_info!(pdev.as_ref(), "M3: resource admission complete\n"); - // An armed T8122 start supplies the values its table leaves out. An unarmed T8122 - // needs a complete table, which it does not have: it is refused here. - let experiment = crate::m3::t8122_start::arm(pdev.as_ref(), soc)?; - if experiment.is_none() { + // Extended T8122 uses its dedicated initialization graph. Keep the + // legacy guarded-start and completeness checks for older interfaces. + let extended=crate::m3::t8122_start::is_t8122(soc) + && crate::m3::t8122_admission::loaded_profile(pdev)?==super::m3_profile::Profile::Extended; + let experiment = if extended {None} else {crate::m3::t8122_start::arm(pdev.as_ref(), soc)?}; + if experiment.is_none() && !extended { soc.require_complete(pdev.as_ref())?; } let experiment = experiment.as_ref(); diff --git a/drivers/gpu/drm/asahi/m3/m3_firmware.rs b/drivers/gpu/drm/asahi/m3/m3_firmware.rs index 6bc87a33aaa052..94a2d3384d08aa 100644 --- a/drivers/gpu/drm/asahi/m3/m3_firmware.rs +++ b/drivers/gpu/drm/asahi/m3/m3_firmware.rs @@ -62,7 +62,7 @@ pub(crate) static KNOWN_IMAGES_T8122: [KnownImage; 1] = [KnownImage { initdata_magic: Some(G15_V14_8_3_INITDATA), }]; -/// Image record for the extended format. The SoC's runtime selection does not use it yet. +/// Exact normalized image identity for the extended firmware interface. pub(crate) static T8122_IMAGES_26: [KnownImage; 1] = [KnownImage { name: "T8122 C0 firmware-compat 26.6.2", uuid: None, @@ -164,7 +164,7 @@ pub(crate) static T8122_LAYOUT: Layout = Layout { pub(crate) const TEXT_SHA256: [u8; 32] = [ 0x11, 0xe4, 0x9f, 0x75, 0xb, 0x67, 0x1a, 0x2b, 0x13, 0xd0, 0x92, 0xdb, 0x1c, 0xbc, 0xa3, 0xa8, 0x77, 0x55, 0x86, 0xd6, 0xb1, 0xe3, 0xaa, 0xe8, 0xa6, 0xa4, 0x64, 0xa7, 0x80, 0x2, 0x93, 0xd5]; -/// T8122 C0, firmware-compat 26.6.2. Selection remains separate from runtime admission. +/// T8122 C0, firmware-compat 26.6.2 segment and loader-mutable bounds. pub(crate) static T8122_LAYOUT_26: Layout = Layout { text_size: 393216, data_size: 1228800, @@ -313,7 +313,10 @@ pub(crate) fn identify_loaded( prelude::*, }; - let layout = soc.firmware.ok_or(ENODEV)?; + let extended=crate::m3::t8122_start::is_t8122(soc) + && crate::m3::t8122_admission::loaded_profile(pdev)?==super::m3_profile::Profile::Extended; + if extended && experiment.is_some() {return Err(EINVAL);} + let layout = if extended {&T8122_LAYOUT_26} else {soc.firmware.ok_or(ENODEV)?}; if !Firmware::matching_layout(layout, &resources) { dev_err!(pdev.as_ref(), "M3 {}: unsupported firmware segment layout\n", soc.gpu_name); return Err(ENODEV); @@ -337,6 +340,7 @@ pub(crate) fn identify_loaded( // The T8122 start experiment identifies the same image, with its own InitData version. let images = match experiment { Some(_) => &KNOWN_IMAGES_T8122_EXPERIMENT[..], + None if extended => &T8122_IMAGES_26[..], None => soc.images, }; let image = crate::m3::m3_board::identify(pdev.as_ref(), bytes, &digest, images).ok_or(ENODEV)?; diff --git a/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs b/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs index 2367e6c7ad557f..0abccad3cb9d64 100644 --- a/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs +++ b/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs @@ -3,6 +3,8 @@ use super::{agx_render::Geometry, agx_render_state::{Program,DepthStencil}, m3_compute_layout::{GpuVa,RegisterSet}, m3_init_layout::{Error,Region}, m3_queue_layout::FirmwareVa, m3_render_sequence::fragment}; pub(crate) const SIZE:usize=0xc73; +pub(crate) const EXTENDED_SIZE:usize=0xca0; +pub(crate) const EXTENDED_FLUSH:usize=0xbdc; pub(crate) const REGISTERS:usize=fragment::REGISTERS; /// The registers G15G does not take; the others keep the G15S order. pub(crate) const G15G_OMIT:[u32;4]=[0x160a8,0x160b8,0xa5a9,0xd429]; @@ -74,16 +76,30 @@ impl Command { /// Check references before touching output. The caller owns validation of /// client resource mappings; this layer serializes their semantic state. pub(crate) fn encode(&self,out:&mut[u8])->Result<(),Error> { - if out.len()Result<(),Error> { + self.encode_format(out,Some(dispatch)) + } + fn encode_format(&self,out:&mut[u8],dispatch:Option)->Result<(),Error> { + let extended=dispatch.is_some(); + let size=if extended {EXTENDED_SIZE} else {SIZE}; + if out.len()=1<<42 || self.register_set!=RegisterSet::G15G {return Err(Error::Address);} + } if self.context>=64 {return Err(Error::Address);} - let seq=self.sequence.at(0,fragment::LENGTH)?; + let sequence_size=if extended {0x370} else {fragment::LENGTH}; + let seq=self.sequence.at(0,sequence_size)?; let alias=self.gpu_alias.get(); - GpuVa::new(alias.checked_add((SIZE-3) as u64).ok_or(Error::Address)?).map_err(|_|Error::Address)?; + GpuVa::new(alias.checked_add(((size-1)&!3) as u64).ok_or(Error::Address)?).map_err(|_|Error::Address)?; let registers=alias+REGISTERS as u64; let s=&self.state;let g=&s.geometry; let layer_metadata=self.heap.get().checked_add(if g.layers>1 {0x200}else{0}).ok_or(Error::Address)?; GpuVa::new(layer_metadata).map_err(|_|Error::Address)?; - let user=super::agx_render_state::compact(self.scene_user.get().checked_add(0x40).ok_or(Error::Address)?) + let user=super::agx_render_state::compact(self.scene_user.get().checked_add(if extended {0} else {0x40}).ok_or(Error::Address)?) .map_err(|_|Error::Address)? & !15; let width=(g.pixels&0xffff)+1;let height=(g.pixels>>16)+1; let tx=(width+31)/32;let ty=(height+31)/32; @@ -116,43 +132,85 @@ impl Command { (0x101c1,0),(0xd469,0),(0x1a0f9,8),(0x1c838,0),(0x1ca28,user), (0xa5a9,0x6000400020),(0xd429,0x200000001), ]; - let bytes=&mut out[..SIZE];bytes.fill(0); + let bytes=&mut out[..size];bytes.fill(0); word(bytes,0,1); // fragment opcode; work counter remains zero word(bytes,0xc,self.context); for (offset,value) in [(0x14,seq),(0x20,self.notifier.get()),(0x28,self.manager.get()), (0x30,self.scene.get()),(0x38,self.empty.get()),(0x40,self.tilemap.get()),(0x780,registers)] {pointer(bytes,offset,value);} - word(bytes,SEQUENCE_LENGTH,fragment::LENGTH as u32); - // The qualified patch path updates register MSAA but retains these - // producer header values. Representation cleanup must not change that. - pointer(bytes,0x48,0x88);word(bytes,0x50,1); - bytes[0x54..0x56].copy_from_slice(&((g.y_blocks>>18) as u16).to_le_bytes()); - bytes[0x56..0x58].copy_from_slice(&((g.x_blocks>>18) as u16).to_le_bytes()); - word(bytes,0x68,s.merge_upper[0]);word(bytes,0x6c,s.merge_upper[1]); - pointer(bytes,0x78,u64::from(g.tiles)); + word(bytes,SEQUENCE_LENGTH,sequence_size as u32); + if extended { + pointer(bytes,4,dispatch.as_ref().map_or(0,|native|native.serial)); + word(bytes,0x50,1); + word(bytes,0x54,g.isp_blocks); + word(bytes,0x60,g.x_blocks); + word(bytes,0x64,g.y_blocks); + word(bytes,0x68,g.blocks); + word(bytes,0x78,g.tiles); + } else { + pointer(bytes,0x48,0x88);word(bytes,0x50,1); + bytes[0x54..0x56].copy_from_slice(&((g.y_blocks>>18) as u16).to_le_bytes()); + bytes[0x56..0x58].copy_from_slice(&((g.x_blocks>>18) as u16).to_le_bytes()); + word(bytes,0x68,s.merge_upper[0]);word(bytes,0x6c,s.merge_upper[1]); + pointer(bytes,0x78,u64::from(g.tiles)); + } let g15g=self.register_set==RegisterSet::G15G; let mut count=0; - for (register,value) in values.into_iter().filter(|(r,_)| !g15g || !G15G_OMIT.contains(r)) { - let n=count;count+=1; - word(bytes,REGISTERS+n*REGISTER_STRIDE,register);pointer(bytes,REGISTERS+n*REGISTER_STRIDE+4,value); + let mut emit=|register,value| { + word(bytes,REGISTERS+count*REGISTER_STRIDE,register); + pointer(bytes,REGISTERS+count*REGISTER_STRIDE+4,value); + count+=1; + }; + for (register,mut value) in values { + if extended { + if matches!(register,0x160b8|0x120a1|0x1c838|0x1ca28) {continue;} + if register==0x10791 {emit(0xa301,0);} + if register==0x16068 {value=(value&!0x3ffff)|0x3717f;} + if matches!(register,0x15109|0x15101) {value&=!3;} + } else if g15g && G15G_OMIT.contains(®ister) {continue;} + emit(register,value); + } + if let Some(native)=&dispatch { + for register in [0x160e0,0x1499,0xa341] {emit(register,fragment::UUID.into());} + emit(0x1c838,0); + emit(0x1ca28,user); + emit(0x10069,native.usc_window); } bytes[0x788..0x78a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x78a..0x78c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); // Start3DJobParameters3: mirror records use their actual packed widths. for (offset,value) in [(0,s.depth_bias),(0x10,s.scissor),(0x20,s.query), - (0x168,s.partial_background.resources),(0x170,s.partial_background.address), + (0x168,if extended {s.background.resources}else{s.partial_background.resources}), + (0x170,if extended {s.background.address}else{s.partial_background.address}), (0x198,s.partial_background.resources),(0x1a0,s.partial_background.address), (0x1a8,s.zls_control),(0x1b8,s.depth.base),(0x1c0,s.depth.stride), (0x1c8,s.depth_compression.stride),(0x1e0,s.depth_compression.base), (0x1d0,s.depth.base),(0x1d8,s.depth.base), (0x1e8,s.stencil.base),(0x1f0,s.stencil.stride),(0x200,s.stencil.base),(0x208,s.stencil.base), (0x1f8,s.stencil_compression.stride),(0x210,s.stencil_compression.base), - (0x26c,s.partial_eot.address),(0x28c,s.partial_eot.address),(0x2c0,u64::from(s.depth_dimensions))] { - pointer(bytes,PARAMETERS+offset,value); + (0x26c,if extended {s.eot.address}else{s.partial_eot.address}),(0x28c,s.partial_eot.address),(0x2c0,u64::from(s.depth_dimensions))] { + pointer(bytes,PARAMETERS+offset+if extended && offset<=0x20 {0x10}else{0},value); } for (offset,value) in [(0x1b0,0x04040404),(0x228,s.mirror_tilebuffer_control), (0x230,s.isp_control),(0x238,width),(0x23c,height),(0x240,0x100000), - (0x248,s.mirror_tile_mode),(0x264,s.partial_eot.resources as u32),(0x284,s.partial_eot.resources as u32), + (0x248,s.mirror_tile_mode),(0x264,if extended {s.eot.resources as u32}else{s.partial_eot.resources as u32}),(0x284,s.partial_eot.resources as u32), (0x298,s.depth_clear),(0x29c,s.stencil_clear),(0x2a0,s.sample_size)] {word(bytes,PARAMETERS+offset,value);} + if let Some(native)=dispatch { + pointer(bytes,0xbc0,self.stamp.get()); + pointer(bytes,0xbc8,self.fw_stamp.get()); + word(bytes,0xbd0,self.stamp_value); + word(bytes,0xbd4,1); + word(bytes,EXTENDED_FLUSH,1); + word(bytes,0xbe0,fragment::UUID); + pointer(bytes,0xc10,self.timestamps[0].get()); + pointer(bytes,0xc18,self.timestamps[1].get()); + pointer(bytes,0xc20,self.user_timestamps[0].get()); + pointer(bytes,0xc28,self.user_timestamps[1].get()); + pointer(bytes,0xc46,self.pool.get()); + bytes[0xc4e]=1; + pointer(bytes,0xc57,native.firmware_aux.get()); + bytes[0xc5f]=native.incarnation; + bytes[0xc84]=1; + } else { word(bytes,0xb88,0x67); // EncoderParams.encoder_id word(bytes,0xb90,u32::MAX); // EncoderParams.unk_mask word(bytes,0xb94,u32::from(s.process_empty_tiles)); @@ -167,6 +225,46 @@ impl Command { pointer(bytes,fragment::COMMAND_TAIL+offset,value); } bytes[fragment::COMMAND_TAIL+0x36]=1;bytes[fragment::COMMAND_TAIL+0x4f]=1; + } Ok(()) } } + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] + fn native_fragment_uses_distinct_program_mirrors_and_completion_tail() { + let fw=|n:u64|FirmwareVa::new(0xffff_fc25_3000_0000+n*0x4000).unwrap(); + let gpu=|n:u64|GpuVa::new(0x10_0300_0000+n*0x4000).unwrap(); + let program=|n:u64|Program{address:0x1_0000_0000+n*0x1000,resources:n}; + let surface=DepthStencil{base:0,stride:0}; + let command=Command {context:3,register_set:RegisterSet::G15G, + sequence:Region::new(fw(0).get(),0x1000).unwrap(),notifier:fw(1),manager:fw(2),scene:fw(3),empty:fw(4), + gpu_alias:GpuVa::new(0x72_1000_4000).unwrap(),tilemap:gpu(0),heap:gpu(1),auxiliary:gpu(2),scene_user:gpu(3), + scratch:gpu(4),pool:fw(5),stamp:fw(6),fw_stamp:fw(7),stamp_value:0x3d000100, + timestamps:[fw(8),fw(9)],user_timestamps:[fw(10),fw(11)], + state:State {geometry:Geometry::new(64,64,super::super::agx_render::Utile::Pixels32, + super::super::agx_render::Utile::Pixels32,1).unwrap(),multisample:0,merge_upper:[0;2], + eot:program(1),background:program(2),partial_eot:program(3),partial_background:program(4), + scissor:0,depth_bias:0,query:0,isp_control:0xc000,tilebuffer_control:1,mirror_tilebuffer_control:1, + tile_mode:0x280,mirror_tile_mode:0x280,depth:surface,stencil:surface,depth_compression:surface, + stencil_compression:surface,zls_control:0,depth_dimensions:0,depth_clear:0,stencil_clear:0, + sample_size:4,process_empty_tiles:false}}; + let dispatch=||super::super::m3_tiler_command::Dispatch26 {incarnation:9,serial:1,peer_stamp:0, + usc_window:1<<32,firmware_aux:fw(12),scene_scratch:gpu(4)}; + let mut bytes=[0xa5;EXTENDED_SIZE+8]; + command.encode_extended(dispatch(),&mut bytes).unwrap(); + let word=|at|u32::from_le_bytes(bytes[at..at+4].try_into().unwrap()); + let pointer=|at|u64::from_le_bytes(bytes[at..at+8].try_into().unwrap()); + assert_eq!(word(0xbd0),command.stamp_value); + assert_eq!(word(EXTENDED_FLUSH),1); + assert_eq!(pointer(0x900),command.state.background.address); + assert_eq!(pointer(0x930),command.state.partial_background.address); + assert_eq!(bytes[0xc5f],9); + assert_eq!(&bytes[EXTENDED_SIZE..],&[0xa5;8]); + let mut short=[0xa5;EXTENDED_SIZE-1]; + assert!(command.encode_extended(dispatch(),&mut short).is_err()); + assert!(short.iter().all(|b|*b==0xa5)); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs b/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs index 684505615adf3b..97c1367b29455d 100644 --- a/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs @@ -61,3 +61,77 @@ impl Manager { Ok(()) } } + +impl Manager { + pub(crate) const EXTENDED_SIZE: usize = 0xc0; + + /// J613's extended manager has both firmware and GPU page-list aliases. + /// The block ring is read through its canonical firmware mapping. + pub(crate) fn encode_extended(&self, pages_fw: FirmwareVa, blocks_fw: FirmwareVa, + hardware_id: u32, output: &mut [u8]) -> Result<(), Error> { + let capacity = self.block_count.checked_add(1).and_then(u32::checked_next_power_of_two) + .ok_or(Error::Size)?; + if hardware_id >= 32 || capacity as usize > self.blocks.size() / 8 { + return Err(Error::Size); + } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let word = |bytes: &mut [u8], offset: usize, value: u32| { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + }; + let address = |bytes: &mut [u8], offset: usize, value: u64| { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + }; + word(bytes, 8, hardware_id); + word(bytes, 12, hardware_id); + address(bytes, 0x1c, pages_fw.get()); + address(bytes, 0x24, self.pages.base()); + word(bytes, 0x2c, self.pages.size() as u32); + word(bytes, 0x30, self.page_count); + word(bytes, 0x34, capacity); + word(bytes, 0x38, self.block_count); + address(bytes, 0x40, blocks_fw.get()); + address(bytes, 0x48, self.control.get()); + word(bytes, 0x50, self.page_count - 1); + word(bytes, 0x54, BLOCK_SIZE as u32); + address(bytes, 0x60, self.counter.get()); + word(bytes, 0x78, self.page_count); + word(bytes, 0x7c, self.page_count); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + fn fixture() -> Manager { + Manager::new(GpuRegion::new(0x5000_0000, 0x4000).unwrap(), + GpuRegion::new(0x6000_0000, 0x4000).unwrap(), + FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), + FirmwareVa::new(0xffff_fc24_0020_0000).unwrap(), 32).unwrap() + } + #[test] fn native_manager_keeps_distinct_aliases_and_hardware_identity() { + let mut bytes = [0xa5; Manager::EXTENDED_SIZE + 8]; + let pages = FirmwareVa::new(0xffff_fc24_0030_0000).unwrap(); + let blocks = FirmwareVa::new(0xffff_fc24_0040_0000).unwrap(); + fixture().encode_extended(pages, blocks, 7, &mut bytes).unwrap(); + let word = |at| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()); + let pointer = |at| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + assert_eq!(word(8), 7); assert_eq!(word(12), 7); + assert_eq!(pointer(0x1c), pages.get()); + assert_eq!(pointer(0x24), 0x5000_0000); + assert_eq!(pointer(0x40), blocks.get()); + assert_eq!(word(0x34), 64); assert_eq!(word(0x38), 32); + assert_eq!(word(0x50), 127); + assert_eq!(pointer(0x80), 0); + assert_eq!(&bytes[Manager::EXTENDED_SIZE..], &[0xa5; 8]); + } + #[test] fn native_manager_rejects_short_storage_and_invalid_identity_before_mutation() { + let fw = FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(); + let mut bytes = [0xa5; Manager::EXTENDED_SIZE]; + assert!(fixture().encode_extended(fw, fw, 32, &mut bytes).is_err()); + assert_eq!(bytes, [0xa5; Manager::EXTENDED_SIZE]); + assert!(fixture().encode_extended(fw, fw, 0, &mut bytes[..MANAGER_SIZE]).is_err()); + assert_eq!(bytes, [0xa5; Manager::EXTENDED_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_pass.rs b/drivers/gpu/drm/asahi/m3/m3_pass.rs index 94c2172e33279b..9c0eb070039cdd 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pass.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pass.rs @@ -8,10 +8,10 @@ use layout::{Field,Space}; pub(crate) struct Pass {buffers:KVec} impl Pass { pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm, - slot:usize,clusters:u32,aliases:&mut KVec)->Result { + slot:usize,clusters:u32,profile:super::m3_profile::Profile,aliases:&mut KVec)->Result { let mut buffers=KVec::with_capacity(layout::COUNT,GFP_KERNEL)?; for field in layout::FIELDS { - let a=layout::board_allocation(slot,field,clusters).map_err(|_|EINVAL)?; + let a=layout::allocation_for(profile,slot,field,clusters).map_err(|_|EINVAL)?; let mut b=match a.space { Space::ClientGpu=>Buffer::at_prot(dev,uat.kernel_vm(),Some((vm,a.address)),None, a.size,prot::PROT_GPU_FW_SHARED_RW,prot::Prot::from_pte(a.access.pte()))?, diff --git a/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs b/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs index a8a2e23c0fbd14..2ec8f8eb212e57 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs @@ -215,3 +215,39 @@ mod tests { } } } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, slot:usize, field:Field, clusters:u32) + -> Result { + let mut owner = board_allocation(slot,field,clusters)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + if owner.space == Space::Firmware { + owner.address = 0xffff_fc25_0000_0000 + (slot * COUNT + field as usize) as u64 * 0x10_0000; + } + match field { + Field::Scene => owner.size = 0x400, + Field::TilerCommand | Field::FragmentCommand => { + let fragment = field == Field::FragmentCommand; + owner.size = if fragment {0xca0} else {0x940}; + owner.gpu_alias = Some(0x72_1000_0000 + slot as u64 * 0x4_0000 + + u64::from(fragment) * 0x4000); + } + Field::TilerToFragment | Field::TilerDependency | Field::FragmentDependency => owner.size = 0x40, + _ => {} + } + Ok(owner) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn extended_command_aliases_keep_whole_disjoint_pages() { + for slot in 0..SLOTS { + for field in [Field::TilerCommand,Field::FragmentCommand] { + let owner = allocation_for(super::super::m3_profile::Profile::Extended,slot,field,1).unwrap(); + assert_eq!(owner.address & 0x3fff,0); + assert_eq!(owner.gpu_alias.unwrap() & 0x3fff,0); + assert!(owner.size <= 0x4000); + } + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_profile.rs b/drivers/gpu/drm/asahi/m3/m3_profile.rs index 004ac2c7d1eef2..04b872ed9ea809 100644 --- a/drivers/gpu/drm/asahi/m3/m3_profile.rs +++ b/drivers/gpu/drm/asahi/m3/m3_profile.rs @@ -32,6 +32,15 @@ impl Profile { } } + /// Keep the diagnostic AP/GPU timeline in its established 24 MHz units. + /// User timestamps and duration calculations retain native firmware units. + pub(crate) const fn profiler_ticks(self, timestamp: u64) -> u64 { + match self { + Self::Legacy => timestamp, + Self::Extended => (timestamp / 1000) * 24 + (timestamp % 1000) * 24 / 1000, + } + } + pub(crate) fn gpu_duration_ns(self, elapsed: u64) -> Option { match self { Self::Legacy => elapsed.checked_mul(1000).map(|value| value / 24), @@ -49,6 +58,18 @@ impl Profile { mod tests { use super::*; + #[test] + fn profiling_uses_one_clock_domain_without_changing_native_durations() { + for value in [0, 1, 999, 1000, 24_000_000, u64::MAX] { + assert_eq!(Profile::Legacy.profiler_ticks(value), value); + assert_eq!( + Profile::Extended.profiler_ticks(value), + ((value as u128) * 24 / 1000) as u64 + ); + } + assert_eq!(Profile::Extended.profiler_ticks(1_000_000_000), 24_000_000); + } + #[test] fn profile_acknowledgement_is_exact_and_file_query_covers_hal() { for profile in [Profile::Legacy, Profile::Extended] { diff --git a/drivers/gpu/drm/asahi/m3/m3_render.rs b/drivers/gpu/drm/asahi/m3/m3_render.rs index e7a02982e96f12..9a9f63d15b784f 100644 --- a/drivers/gpu/drm/asahi/m3/m3_render.rs +++ b/drivers/gpu/drm/asahi/m3/m3_render.rs @@ -26,7 +26,7 @@ pub(crate) struct Render { passes:KVec, init_bm:Buffer, // All active mappings above must drop before their ASID binding. _binding:mmu::VmBind, - stats: Region, sequence_bytes: KVec, tiler_bytes: KVec, fragment_bytes: KVec, + stats: [Region;2], profile:super::m3_profile::Profile, sequence_bytes: KVec, tiler_bytes: KVec, fragment_bytes: KVec, last_progress_ns:i64, draw:u64, heads:[u16;2], cached_views:KVec, slot:usize, batch_count:usize, early_count:u32, @@ -67,37 +67,45 @@ const RENDER_QUEUES: [QueueObjects; 2] = [ scratch: s::FRAGMENT_SCRATCH, tiler: false, uuid: 0x3d0000 }, ]; impl Render { - pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm,stats:Region,clusters:u32, - registers:crate::m3::m3_compute_layout::RegisterSet)->Result { + pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm,stats:[Region;2],clusters:u32, + registers:crate::m3::m3_compute_layout::RegisterSet,profile:super::m3_profile::Profile)->Result { let lower_tlbi=crate::m3::m3_memory::LowerTlbi; let binding=uat.bind(vm)?; let mut objects=KVec::new();let mut aliases=KVec::new(); for index in 0..s::COUNT { - let a=s::allocation(index).map_err(|_|EINVAL)?; + let a=s::allocation_for(profile,index).map_err(|_|EINVAL)?; let protection=prot::Prot::from_pte(a.access.pte()); let b=match a.space { Space::ClientGpu=>Buffer::at_prot(dev,uat.kernel_vm(),Some((vm,a.address)),None, a.size,prot::PROT_GPU_FW_SHARED_RW,protection)?, - Space::Firmware=>Buffer::at_prot(dev,uat.kernel_vm(),None,Some(a.address), + Space::Firmware=>Buffer::at_prot(dev,uat.kernel_vm(), + a.gpu_alias.map(|address|(uat.kernel_lower_vm(),address)),Some(a.address), a.size,protection,prot::PROT_GPU_SHARED_RW)?, }; + let mut b=b; + if a.gpu_alias.is_some() { aliases.push(b.map_gpu_view(vm)?,GFP_KERNEL)?; } objects.push(b,GFP_KERNEL)?; } // InitBM is shared, not part of a pass. Preserve its qualified mapping. - let init_bm=Buffer::at_prot(dev,uat.kernel_vm(),None,Some(0xfffffc200062bfe0), + let init_bm=Buffer::at_prot(dev,uat.kernel_vm(),None,Some(if profile==super::m3_profile::Profile::Extended {0xffff_fc24_4000_0000} + else {0xfffffc200062bfe0}), sync::INIT_BM_SIZE,prot::PROT_FW_SHARED_RW,prot::PROT_GPU_SHARED_RW)?; let mut passes=KVec::with_capacity(pass_layout::SLOTS,GFP_KERNEL)?; - for slot in 0..pass_layout::SLOTS {passes.push(Pass::new(dev,uat,vm,slot,clusters,&mut aliases)?,GFP_KERNEL)?;} + for slot in 0..pass_layout::SLOTS {passes.push(Pass::new(dev,uat,vm,slot,clusters,profile,&mut aliases)?,GFP_KERNEL)?;} let mut sequence_bytes=KVec::new(); sequence_bytes.resize(sequence::STORAGE,0u8,GFP_KERNEL)?; let mut tiler_bytes=KVec::new(); - tiler_bytes.resize(tiler::SIZE,0u8,GFP_KERNEL)?; + tiler_bytes.resize(if profile==super::m3_profile::Profile::Extended {tiler::EXTENDED_SIZE} else {tiler::SIZE},0u8,GFP_KERNEL)?; let mut fragment_bytes=KVec::new(); - fragment_bytes.resize(fragment::SIZE,0u8,GFP_KERNEL)?; - let mut job=Self{passes,init_bm,stats,sequence_bytes,tiler_bytes,fragment_bytes,objects,_aliases:aliases,_binding:binding,last_progress_ns:0,draw:0,heads:[0;2],cached_views:KVec::new(),slot:0,batch_count:0,early_count:0,stage_timestamps:[true;2],base:0,checkpoint:(0,[0;2],0,0,0,0),clusters,registers,_lower_tlbi:lower_tlbi}; + fragment_bytes.resize(if profile==super::m3_profile::Profile::Extended {fragment::EXTENDED_SIZE} else {fragment::SIZE},0u8,GFP_KERNEL)?; + let mut job=Self{passes,init_bm,stats,profile,sequence_bytes,tiler_bytes,fragment_bytes,objects,_aliases:aliases,_binding:binding,last_progress_ns:0,draw:0,heads:[0;2],cached_views:KVec::new(),slot:0,batch_count:0,early_count:0,stage_timestamps:[true;2],base:0,checkpoint:(0,[0;2],0,0,0,0),clusters,registers,_lower_tlbi:lower_tlbi}; // Lists and the matching manager are built before InitBM publication. job.initialize_parameter_buffer()?; - for pool in &UMA_POOLS { pool.initialize(&mut job.objects)?; } + for (index,pool) in UMA_POOLS.iter().enumerate() { + if profile==super::m3_profile::Profile::Extended { + pool.initialize_extended(&mut job.objects,33+index as u8,false)?; + } else {pool.initialize(&mut job.objects)?;} + } job.initialize_state()?; job.initialize_queues()?; dev_info!(dev.as_ref(),"M3 native render storage initialized, PB={} MiB\n",PB_BLOCKS as usize*PB_BLOCK_SIZE/(1024*1024)); @@ -113,8 +121,14 @@ impl Render { let control = queue::FirmwareVa::new(self.objects[BLOCK_CONTROL].va()).map_err(|_| EINVAL)?; let counter = queue::FirmwareVa::new(self.objects[s::BM_COUNTER].va()).map_err(|_| EINVAL)?; let manager = parameter::Manager::new(pages, blocks, control, counter, PB_BLOCKS).map_err(|_| EINVAL)?; - let mut bytes = [0; parameter::MANAGER_SIZE]; - manager.encode(&mut bytes).map_err(|_| EINVAL)?; + let extended=self.profile==super::m3_profile::Profile::Extended; + let mut bytes = [0; parameter::Manager::EXTENDED_SIZE]; + let length=if extended { + manager.encode_extended(queue::FirmwareVa::new(self.objects[PARAMETER_PAGE_LIST].va()).map_err(|_|EINVAL)?, + queue::FirmwareVa::new(self.objects[PARAMETER_BLOCK_LIST].va()).map_err(|_|EINVAL)?,0,&mut bytes) + .map_err(|_|EINVAL)?; + parameter::Manager::EXTENDED_SIZE + } else {manager.encode(&mut bytes).map_err(|_|EINVAL)?;parameter::MANAGER_SIZE}; for group in 0..PB_GROUPS { let buffer = &self.objects[PB_FIRST + group]; let backing = parameter::Backing::new(GpuRegion::new(buffer.gpu_va()?, buffer.size()).map_err(|_| EINVAL)?) @@ -128,18 +142,30 @@ impl Render { backing.page(page).map_err(|_| EINVAL)?)?; } } - self.objects[BUFFER_MANAGER].write(0, &bytes) + self.objects[BUFFER_MANAGER].write(0, &bytes[..length]) } fn initialize_state(&mut self) -> Result { let address = |owner: usize| queue::FirmwareVa::new(self.objects[owner].va()).map_err(|_| EINVAL); let notification = state::Notifier { threshold: address(s::EVENT_COUNT)?, context: self._binding.slot() }; let list_owner = address(JOB_LIST)?; let mut context = [0; state::CONTEXT_SIZE]; - state::context(&mut context).map_err(|_| EINVAL)?; - self.objects[GPU_CONTEXT].write(0, &context)?; - let mut notifier = [0; state::NOTIFIER_SIZE]; - notification.encode(&mut notifier).map_err(|_| EINVAL)?; - self.objects[NOTIFIER].write(0, ¬ifier)?; + if self.profile==super::m3_profile::Profile::Extended { + state::context_extended(&mut context).map_err(|_|EINVAL)?; + self.objects[GPU_CONTEXT].write(0,&context[..state::EXTENDED_CONTEXT_SIZE])?; + for engine in 0..2 { + let value=state::Notifier {threshold:queue::FirmwareVa::new(self.objects[s::EVENT_COUNT].va()+engine as u64*8).map_err(|_|EINVAL)?, + context:self._binding.slot()}; + let mut notifier=[0;state::EXTENDED_NOTIFIER_SIZE]; + value.encode_extended(&mut notifier).map_err(|_|EINVAL)?; + self.objects[NOTIFIER].write(engine*state::EXTENDED_NOTIFIER_SIZE,¬ifier)?; + } + } else { + state::context(&mut context).map_err(|_| EINVAL)?; + self.objects[GPU_CONTEXT].write(0, &context)?; + let mut notifier = [0; state::NOTIFIER_SIZE]; + notification.encode(&mut notifier).map_err(|_| EINVAL)?; + self.objects[NOTIFIER].write(0, ¬ifier)?; + } let mut list = [0; state::JOB_LIST_SIZE]; state::job_list(&mut list, list_owner).map_err(|_| EINVAL)?; self.objects[JOB_LIST].write(0, &list)?; @@ -157,6 +183,10 @@ impl Render { state::counter(bytes, value).map_err(|_| EINVAL)?; self.objects[owner].write(0, bytes)?; } + if self.profile==super::m3_profile::Profile::Extended { + self.objects[s::EVENT_COUNT].fill(0)?; + self.objects[s::BM_COUNTER].fill(0)?; + } Ok(()) } fn initialize_queues(&mut self) -> Result { @@ -172,8 +202,13 @@ impl Render { }else{[self.passes[0].get(P::TilerToFragment).va(),self.passes[0].get(P::FragmentCommand).va()]}; let seeds=[queue::FirmwareVa::new(seed_addresses[0]).map_err(|_|EINVAL)?, queue::FirmwareVa::new(seed_addresses[1]).map_err(|_|EINVAL)?]; - let mut bytes = [0; queue::INFO_SIZE]; - info.encode(&mut bytes).map_err(|_| EINVAL)?; + let mut bytes=KVec::new(); + bytes.resize(if self.profile==super::m3_profile::Profile::Extended { + queue::QueueInfo::EXTENDED_SIZE + } else {queue::INFO_SIZE},0u8,GFP_KERNEL)?; + if self.profile==super::m3_profile::Profile::Extended { + info.encode_extended(address(owner.info)?,&mut bytes).map_err(|_|EINVAL)?; + } else {info.encode(&mut bytes).map_err(|_|EINVAL)?;} self.objects[owner.info].write(0, &bytes)?; let mut state = [0; queue::STATE_SIZE]; queue::RingState { capacity: queue::QUALIFIED_CAPACITY, done: 0, read: 0, write: 0 } @@ -187,9 +222,12 @@ impl Render { Ok(()) } fn write_barrier(&mut self, owner: P, command: sync::Barrier) -> Result { - let mut bytes = [0; sync::BARRIER_SIZE]; - command.encode(&mut bytes).map_err(|_| EINVAL)?; - self.passes[self.slot].get_mut(owner).write(0, &bytes) + let mut bytes=[0;sync::Barrier::EXTENDED_SIZE]; + let length=if self.profile==super::m3_profile::Profile::Extended { + command.encode_extended(&mut bytes).map_err(|_|EINVAL)?; + sync::Barrier::EXTENDED_SIZE + } else {command.encode(&mut bytes).map_err(|_|EINVAL)?;sync::BARRIER_SIZE}; + self.passes[self.slot].get_mut(owner).write(0,&bytes[..length]) } // Construct synchronization from the actual owners after resetting a pass, // before publishing its ring entries. InitBM is shared and dispatched only @@ -201,9 +239,11 @@ impl Render { let ta_stamp = address(s::TA_FW_STAMP)?; let fs_stamp = address(s::FRAGMENT_FW_STAMP)?; let mut init = [0; sync::INIT_BM_SIZE]; - sync::InitBufferManager { context: self._binding.slot(), slot: 0, - block_count: PB_BLOCKS, manager, stamp: timeline::stamp(s::STAMP_TA - 256, 1) } - .encode(&mut init).map_err(|_| EINVAL)?; + let value=sync::InitBufferManager { context: self._binding.slot(), slot: 0, + block_count: PB_BLOCKS, manager, stamp: timeline::stamp(s::STAMP_TA - 256, 1) }; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(&mut init).map_err(|_|EINVAL)?; + } else {value.encode(&mut init).map_err(|_|EINVAL)?;} self.init_bm.write(0, &init)?; let ta_value = timeline::stamp(s::STAMP_TA - 256, draw); let fs_value = timeline::stamp(s::STAMP_FRAGMENT - 256, draw); @@ -225,7 +265,7 @@ impl Render { } // Prepare each private command after its retired slot is reset. The // reusable host staging allocation is copied before the next slot is built. - fn prepare_tiler(&mut self,r:agx_uapi::UapiRenderCommand,draw:u64)->Result { + fn prepare_tiler(&mut self,r:agx_uapi::UapiRenderCommand,usc:u64,draw:u64)->Result { use crate::m3::m3_compute_layout::GpuVa; let pass=&self.passes[self.slot]; let shared=|index:usize| queue::FirmwareVa::new(self.objects[index].va()).map_err(|_|EINVAL); @@ -243,7 +283,7 @@ impl Render { let value=tiler::Command { context:self._binding.slot(),register_set:self.registers, notifier:shared(NOTIFIER)?,manager:shared(BUFFER_MANAGER)?, - scene:fw(P::Scene)?,empty:shared(547)?,gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?, + scene:fw(P::Scene)?,empty:if self.profile==super::m3_profile::Profile::Extended {queue::FirmwareVa::new(pass.get(P::Scene).va()+0x100).map_err(|_|EINVAL)?} else {shared(547)?},gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?, sequence:Region::new(seq.va(),seq.size()).map_err(|_|EINVAL)?,pool:shared(UMA_POOLS[0].manager)?, scratch:fw(P::TilerScratch)?,stamp:shared(s::TA_STAMP)?,fw_stamp:shared(s::TA_FW_STAMP)?, stamp_value:timeline::stamp(s::STAMP_TA - 256, draw),timestamps:[fw(P::TilerStart)?,fw(P::TilerEnd)?], @@ -253,7 +293,12 @@ impl Render { vdm:crate::m3::agx_render_state::compact(r.vdm_base).map_err(|_|EINVAL)?, multisample:r.multisample_control,ppp:r.ppp_control,process_empty_tiles:r.flags&2!=0, }; - value.encode(&mut self.tiler_bytes).map_err(|_|EINVAL)?; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(tiler::Dispatch26 {incarnation:self._binding.generation(),serial:draw.checked_mul(2).ok_or(EOVERFLOW)?, + peer_stamp:timeline::stamp(s::STAMP_FRAGMENT-256,draw),usc_window:usc, + firmware_aux:queue::FirmwareVa::new(self.objects[UMA_POOLS[0].manager].va()+0x100).map_err(|_|EINVAL)?, + scene_scratch:gpu(P::FragmentScratch)?},&mut self.tiler_bytes).map_err(|_|EINVAL)?; + } else {value.encode(&mut self.tiler_bytes).map_err(|_|EINVAL)?;} self.passes[self.slot].get_mut(P::TilerCommand).write(0,&self.tiler_bytes) } fn prepare_sequences(&mut self,draw:u64)->Result { @@ -266,14 +311,21 @@ impl Render { let notifier=&self.objects[NOTIFIER]; let owners=sequence::Owners { command:region(if fs{P::FragmentCommand}else{P::TilerCommand})?,sequence:region(target)?, - stats:self.stats,notifier:Region::new(notifier.va(),notifier.size()).map_err(|_|EINVAL)?, + stats:self.stats[usize::from(fs)],notifier:Region::new(notifier.va()+ + if fs && self.profile==super::m3_profile::Profile::Extended {0x200} else {0}, + if self.profile==super::m3_profile::Profile::Extended {0x200} else {notifier.size()}).map_err(|_|EINVAL)?, queue:shared(if fs{s::FRAGMENT_QUEUE}else{s::TA_QUEUE})?,scene:fw(P::Scene)?, manager:shared(BUFFER_MANAGER)?,pool:shared(UMA_POOLS[usize::from(fs)].manager)?, fw_stamp:shared(if fs{s::FRAGMENT_FW_STAMP}else{s::TA_FW_STAMP})?, context:self._binding.slot(),stamp:timeline::stamp((if fs{s::STAMP_FRAGMENT}else{s::STAMP_TA}) - 256, draw), }; let timed=self.stage_timestamps[usize::from(fs)]; - let length=if fs { + let length=if self.profile==super::m3_profile::Profile::Extended { + let pool=&self.objects[UMA_POOLS[usize::from(fs)].manager]; + let auxiliary=super::m3_compute_layout::GpuVa::new(pool.gpu_va()?+if fs{0x200}else{0x100}).map_err(|_|EINVAL)?; + let serial=draw.checked_mul(2).and_then(|v|v.checked_sub(u64::from(fs))).ok_or(EOVERFLOW)?; + owners.encode_extended(fs,auxiliary,serial,&mut self.sequence_bytes).map_err(|_|EINVAL)? + } else if fs { let b=pass.get(P::FragmentScratch); let scratch=GpuRegion::new(b.gpu_va()?,b.size()).map_err(|_|EINVAL)?; sequence::Fragment{owners,scratch,queue_command_count:timeline::previous(draw)}.encode_timestamps(&mut self.sequence_bytes,timed).map_err(|_|EINVAL)? @@ -290,6 +342,9 @@ impl Render { fn patch_context(&mut self)->Result { let context=self._binding.slot(); self.objects[NOTIFIER].u32(state::NOTIFIER_CONTEXT, context)?; + if self.profile==super::m3_profile::Profile::Extended { + self.objects[NOTIFIER].u32(0x200+state::NOTIFIER_CONTEXT,context)?; + } Ok(()) } /// Pass geometry; TPC storage is per tiler accelerator, one per GPU cluster. @@ -320,7 +375,8 @@ impl Render { let value=fragment::Command { context:self._binding.slot(),register_set:self.registers, sequence:Region::new(seq.va(),seq.size()).map_err(|_|EINVAL)?, - notifier:shared(NOTIFIER)?,manager:shared(BUFFER_MANAGER)?,scene:fw(P::Scene)?,empty:shared(547)?, + notifier:queue::FirmwareVa::new(self.objects[NOTIFIER].va()+ + if self.profile==super::m3_profile::Profile::Extended {0x200} else {0}).map_err(|_|EINVAL)?,manager:shared(BUFFER_MANAGER)?,scene:fw(P::Scene)?,empty:if self.profile==super::m3_profile::Profile::Extended {queue::FirmwareVa::new(pass.get(P::Scene).va()+0x100).map_err(|_|EINVAL)?} else {shared(547)?}, gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?,tilemap:gpu(P::Tilemap)?, heap:gpu(P::HeapMetadata)?,auxiliary:gpu(P::Auxiliary)?,scene_user:gpu(P::SceneUser)?, scratch:gpu(P::FragmentScratch)?,pool:shared(UMA_POOLS[1].manager)?,stamp:shared(s::FRAGMENT_STAMP)?, @@ -341,15 +397,34 @@ impl Render { stencil_clear:r.stencil_clear,sample_size:u32::from(r.sample_size),process_empty_tiles:r.flags&2!=0, }, }; - value.encode(&mut self.fragment_bytes).map_err(|_|EINVAL)?; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(tiler::Dispatch26 {incarnation:self._binding.generation(),serial:draw.checked_mul(2).and_then(|v|v.checked_sub(1)).ok_or(EOVERFLOW)?, + peer_stamp:0,usc_window:usc, + firmware_aux:queue::FirmwareVa::new(self.objects[UMA_POOLS[1].manager].va()+0x200).map_err(|_|EINVAL)?, + scene_scratch:gpu(P::FragmentScratch)?},&mut self.fragment_bytes).map_err(|_|EINVAL)?; + } else {value.encode(&mut self.fragment_bytes).map_err(|_|EINVAL)?;} self.passes[self.slot].get_mut(P::FragmentCommand).write(0,&self.fragment_bytes) } + pub(crate) fn set_attachments(&mut self,attachments:&[crate::m3::agx_attachments::Attachments;2])->Result { + // Preserve the legacy renderer's existing packet format. The native + // interface consumes separate attachment apertures for both stages. + if self.profile!=super::m3_profile::Profile::Extended {return Ok(());} + for (fragment,list) in attachments.iter().enumerate() { + let field=if fragment==0 {P::TilerSequence}else{P::FragmentSequence}; + let buffer=self.passes[self.slot].get_mut(field); + sequence::patch_extended_attachments(fragment!=0,list,|offset,data|buffer.write(offset,data))?; + } + crate::m3::agx_memory::publish();Ok(()) + } pub(crate) fn set_user_timestamps(&mut self,addresses:[[u64;2];2])->Result { if addresses.iter().enumerate().any(|(stage,pair)|*pair!=[0;2] && !self.stage_timestamps[stage]) {return Err(EINVAL);} for (stage,(command,sequence,offset,start,end)) in [ (P::TilerCommand,P::TilerSequence,sequence::ta::USER_PAIR,sequence::ta::TIMESTAMP_START,sequence::ta::TIMESTAMP_END), (P::FragmentCommand,P::FragmentSequence,sequence::fragment::USER_PAIR,sequence::fragment::TIMESTAMP_START,sequence::fragment::TIMESTAMP_END) ].into_iter().enumerate() { + let (offset,start,end)=if self.profile==super::m3_profile::Profile::Extended { + if stage==0 {(0x8d8,0x1cc,0x21c)} else {(0xc20,0x1e4,0x234)} + } else {(offset,start,end)}; let pair=addresses[stage]; if !self.stage_timestamps[stage] {continue;} self.passes[self.slot].get_mut(command).u64(offset,pair[0])?; @@ -365,19 +440,19 @@ impl Render { pub(crate) fn gpu_ns(&mut self,base:usize,count:usize)->Result { let start=self.passes[base].get_mut(P::TilerStart).read_u64(0)?; let end=self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?; - Ok(if start!=0 && end>=start {(end-start)*1000/24} else {0}) + self.profile.gpu_duration_ns(if start!=0 && end>=start {end-start} else {0}).ok_or(EOVERFLOW) } /// The batch's first tiler start and last fragment end, in 24 MHz GPU timestamp ticks. pub(crate) fn gpu_ticks(&mut self,base:usize,count:usize)->Result<(u64,u64)> { - Ok((self.passes[base].get_mut(P::TilerStart).read_u64(0)?, - self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?)) + Ok((self.profile.profiler_ticks(self.passes[base].get_mut(P::TilerStart).read_u64(0)?), + self.profile.profiler_ticks(self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?))) } /// Read retained timestamps only after exact batch retirement. The first /// TA and final fragment delimit the batch in the shared 24 MHz counter. pub(crate) fn batch_gpu_span(&mut self)->Result<[u64;2]> { if self.batch_count == 0 {return Err(EINVAL);} - Ok([self.passes[self.base].get_mut(P::TilerStart).read_u64(0)?, - self.passes[Self::at(self.base,self.batch_count-1)].get_mut(P::FragmentEnd).read_u64(0)?]) + let (start,end)=self.gpu_ticks(self.base,self.batch_count)?; + Ok([start,end]) } pub(crate) fn stage_ns(&mut self,base:usize,count:usize)->Result<[u64;3]> { let mut total=[0;3]; @@ -390,7 +465,7 @@ impl Render { pub(crate) fn slot_stage_ns(&mut self,base:usize,count:usize,index:usize)->Result<[u64;3]> { if index>=count {return Err(EINVAL);} let slot=Self::at(base,index); - let duration=|a:u64,b:u64| if a!=0 && b>=a {(b-a)*1000/24} else {0}; + let duration=|a:u64,b:u64| self.profile.gpu_duration_ns(if a!=0 && b>=a {b-a} else {0}).unwrap_or(0); let fs_start=self.passes[slot].get_mut(P::FragmentStart).read_u64(0)?; let fs_end=self.passes[slot].get_mut(P::FragmentEnd).read_u64(0)?; let ta_start=self.passes[slot].get_mut(P::TilerStart).read_u64(0)?; @@ -415,7 +490,9 @@ impl Render { } /// Whether any draw has been published (the first one initializes the buffer manager). pub(crate) fn started(&self)->bool {self.draw>0} - pub(crate) fn first(&self)->bool {self.draw==self.batch_count as u64} + pub(crate) fn first(&self)->bool { + self.profile==super::m3_profile::Profile::Legacy && self.draw==self.batch_count as u64 + } /// Start a batch in pass slots `base..`. With `overlap`, earlier batches may still run: then /// the VM must already be bound, no storage may grow and the slots must not be in flight /// (the caller keeps the in-flight passes plus this batch within SLOTS). @@ -447,13 +524,15 @@ impl Render { let binding=uat.bind(vm)?; let mut views=KVec::new();let mut aliases=KVec::new(); for i in 0..s::COUNT { - if s::allocation(i).map_err(|_|EINVAL)?.space==Space::ClientGpu { + let a=s::allocation_for(self.profile,i).map_err(|_|EINVAL)?; + if a.gpu_alias.is_some() {aliases.push(self.objects[i].map_gpu_view(vm)?,GFP_KERNEL)?;} + if a.space==Space::ClientGpu { views.push((ViewOwner::Shared(i),self.objects[i].map_gpu_view(vm)?),GFP_KERNEL)?; } } for (slot,pass) in self.passes.iter_mut().enumerate() { for field in pass_layout::FIELDS { - let a=pass_layout::board_allocation(slot,field,self.clusters).map_err(|_|EINVAL)?; + let a=pass_layout::allocation_for(self.profile,slot,field,self.clusters).map_err(|_|EINVAL)?; if a.space==Space::ClientGpu {views.push((ViewOwner::Pass{slot,field},pass.get_mut(field).map_gpu_view(vm)?),GFP_KERNEL)?;} if a.gpu_alias.is_some() {aliases.push(pass.get_mut(field).map_gpu_view(vm)?,GFP_KERNEL)?;} } @@ -498,12 +577,22 @@ impl Render { self.base=base; Ok(()) } + fn publish_counts(&mut self, draw:u64) -> Result { + if self.profile==super::m3_profile::Profile::Extended { + for offset in [0,8] {self.objects[s::EVENT_COUNT].u64(offset,draw)?;} + for counter in [TA_POOL_COUNTER,FRAGMENT_POOL_COUNTER] {self.objects[counter].u64(0,draw)?;} + } else { + self.objects[s::BM_COUNTER].u32(0,draw as u32)?; + self.objects[s::EVENT_COUNT].u32(0,timeline::events(draw,2))?; + } + Ok(()) + } + /// Restore producer state only when no batch is in flight. Running firmware can /// consume append's WRITE updates without a new doorbell, so overlap cannot abort. pub(crate) fn abort_batch(&mut self)->Result { (self.draw,self.heads,self.batch_count,self.slot,self.early_count,self.base)=self.checkpoint; - self.objects[s::BM_COUNTER].u32(0,self.draw as u32)?; - self.objects[s::EVENT_COUNT].u32(0,timeline::events(self.draw,2))?; + self.publish_counts(self.draw)?; for (stage,pointers) in [s::TA_POINTERS,s::FRAGMENT_POINTERS].into_iter().enumerate() { self.objects[pointers].u32(queue::WRITE,u32::from(self.heads[stage]))?; } @@ -547,8 +636,20 @@ impl Render { self.draw=self.draw.checked_add(1).ok_or(EOVERFLOW)?; self.batch_count+=1; let draw=self.draw; - self.objects[s::BM_COUNTER].u32(0,draw as u32)?; - self.objects[s::EVENT_COUNT].u32(0,timeline::events(draw,2))?; + self.publish_counts(draw)?; + if self.profile==super::m3_profile::Profile::Extended { + let pass=&self.passes[self.slot]; + let scratch=pass.get(P::FragmentScratch); + let scratch_gpu=GpuRegion::new(scratch.gpu_va()?,scratch.size()).map_err(|_|EINVAL)?; + let scratch_fw=queue::FirmwareVa::new(scratch.va()).map_err(|_|EINVAL)?; + let list=pass.get(P::SceneUser); + let list=GpuRegion::new(list.gpu_va()?,list.size()).map_err(|_|EINVAL)?; + let metadata=pass.get(P::Scene); + let owner=Region::new(metadata.va(),metadata.size()).map_err(|_|EINVAL)?; + let mut bytes=[0;scene::EXTENDED_SIZE]; + scene::encode_extended(&mut bytes,owner,scratch_gpu,scratch_fw,list).map_err(|_|EINVAL)?; + self.passes[self.slot].get_mut(P::Scene).write(0,&bytes)?; + } else { let entry_offset = (self.draw % u64::from(MANAGER_SCENE_COUNT)) as usize * 4; let scene_list = &mut self.objects[s::BM_SCENES]; if entry_offset + 4 > scene_list.size() { return Err(ERANGE); } @@ -560,7 +661,8 @@ impl Render { let mut bytes = [0; scene::SIZE]; scene::Scene { manager_entry, manager_misc, user_buffer }.encode(&mut bytes).map_err(|_| EINVAL)?; self.passes[self.slot].get_mut(P::Scene).write(0, &bytes)?; - self.prepare_tiler(r,draw)?; + } + self.prepare_tiler(r,usc,draw)?; self.patch_context()?; self.prepare_fragment(r,usc,draw)?; self.prepare_sequences(draw)?; @@ -655,7 +757,13 @@ impl Render { if self.objects[stamp].read_u32(0)?!=value || self.objects[fw].read_u32(0)?!=value || self.objects[pointers].read_u32(queue::DONE)?!=u32::from(head) || self.objects[pointers].read_u32(queue::READ)?!=u32::from(head) {return Ok(false);} - } Ok(true) + } + if self.profile==super::m3_profile::Profile::Extended && self.draw!=0 { + for pool in UMA_POOLS { + if self.objects[pool.manager].read_u64(0x54)?!=self.draw {return Ok(false);} + } + } + Ok(true) } pub(crate) fn log(&mut self,dev:&driver::AsahiDrmDevice)->Result { // Retain allocator state as well as payloads: an exhausted parameter diff --git a/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs b/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs index f9a0483e77b21b..3d421e99066929 100644 --- a/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs +++ b/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs @@ -184,3 +184,171 @@ impl Fragment { Ok(if timed {LENGTH} else {without_timestamps(bytes,TIMESTAMP_START,FINALIZE,LENGTH,0xa4)}) } } + +impl Owners { + pub(crate) fn encode_extended(&self, fragment:bool, + auxiliary:super::m3_compute_layout::GpuVa, serial:u64, output:&mut[u8]) -> Result { + if self.context>=64 || serial==0 || self.stamp==0 {return Err(Error::Address);} + let command=self.command.at(0,if fragment {0xca0}else{0x940})?; + let sequence=self.sequence.at(0,STORAGE)?; + let activity=self.stats.at(0,8)?; + let scene_tail=self.scene.get().checked_add(0x54).ok_or(Error::Address)?; + let (clock,pair,aux_time,elapsed)=if fragment {(0xc08,0xc10,0xc70,0xb84)}else{(0x8c0,0x8c8,0x928,0x83c)}; + let first=if fragment {0x1e4}else{0x1cc}; + let finalize=if fragment {0x280}else{0x268}; + let pool=if fragment {0x1b0}else{0x190}; + let length=if fragment {0x370}else{0x300}; + let uuid=if fragment {fragment::UUID}else{ta::UUID}; + let bytes=output.get_mut(..STORAGE).ok_or(Error::Size)?; + bytes.fill(0); + word(bytes,0,if fragment {7}else{5}); + pointer(bytes,0x14,command+if fragment {0x80}else{0x40}); + pointer(bytes,pool,self.pool.get()); + pointer(bytes,pool+0x18,auxiliary.get()); + pointer(bytes,pool+0x28,serial); + word(bytes,pool+0x30,u32::from(fragment)); + if fragment { + pointer(bytes,0x1c,self.scene.get()); + pointer(bytes,0x24,activity); + pointer(bytes,0x2c,command+0xb70); + pointer(bytes,0x34,scene_tail); + pointer(bytes,0x3c,command+0xbb0); + pointer(bytes,0x44,command+0xbb4); + pointer(bytes,0x4c,self.queue.get()); + pointer(bytes,0x54,command); + word(bytes,0x5c,self.context); + word(bytes,0x60,1); + pointer(bytes,0x7c,command+0xa58); + pointer(bytes,0x84,command+0xbf0); + word(bytes,0xa0,uuid); + } else { + pointer(bytes,0x1c,self.manager.get()); + pointer(bytes,0x24,self.scene.get()); + pointer(bytes,0x2c,activity); + pointer(bytes,0x34,self.queue.get()); + pointer(bytes,0x3c,command+0x85c); + word(bytes,0x44,self.context); + word(bytes,0x48,1); + pointer(bytes,0x64,command+0x784); + pointer(bytes,0x6c,command+0x8a8); + word(bytes,0x7c,uuid); + } + for (at,opcode,selected) in [(first,0x8000_0003,pair),(first+0x50,3,pair+8)] { + word(bytes,at,opcode); + pointer(bytes,at+4,command+clock); + pointer(bytes,at+0xc,command+pair); + pointer(bytes,at+0x14,command+selected); + pointer(bytes,at+0x1c,self.queue.get()); + pointer(bytes,at+0x2c,command+aux_time); + pointer(bytes,at+0x34,command+elapsed); + pointer(bytes,at+0x44,uuid.into()); + } + word(bytes,first+0x4c,1); + word(bytes,finalize,if fragment {8}else{6}); + if fragment { + word(bytes,finalize+4,uuid); + pointer(bytes,finalize+0xc,self.fw_stamp.get()); + word(bytes,finalize+0x14,self.stamp); + pointer(bytes,finalize+0x1c,self.scene.get()); + pointer(bytes,finalize+0x24,self.manager.get()); + word(bytes,finalize+0x2c,1); + pointer(bytes,finalize+0x30,activity); + pointer(bytes,finalize+0x38,command+0xbb0); + pointer(bytes,finalize+0x40,command+0xbb4); + pointer(bytes,finalize+0x48,command+0xb70); + pointer(bytes,finalize+0x50,self.queue.get()); + pointer(bytes,finalize+0x58,command); + word(bytes,finalize+0x60,self.context); + pointer(bytes,finalize+0x64,command+0xa58); + pointer(bytes,finalize+0x98,sequence+pool as u64); + word(bytes,finalize+0xa0,(-(finalize as i32))as u32); + pointer(bytes,finalize+0xa5,command+0xc60); + word(bytes,finalize+0xad,1); + bytes[finalize+0xb1]=1; + // J613 terminates this record after the 16-bit event field. + bytes[finalize+0xb2..finalize+0xb4].copy_from_slice(&1u16.to_le_bytes()); + word(bytes,finalize+0xb4,0x4000_0002); + } else { + pointer(bytes,finalize+4,self.scene.get()); + pointer(bytes,finalize+0xc,self.manager.get()); + pointer(bytes,finalize+0x14,activity); + pointer(bytes,finalize+0x1c,self.queue.get()); + pointer(bytes,finalize+0x24,command+0x85c); + word(bytes,finalize+0x2c,self.context); + pointer(bytes,finalize+0x34,command+0x784); + word(bytes,finalize+0x40,uuid); + pointer(bytes,finalize+0x48,self.fw_stamp.get()); + word(bytes,finalize+0x50,self.stamp); + pointer(bytes,finalize+0x78,sequence+pool as u64); + word(bytes,finalize+0x80,(-(finalize as i32))as u32); + pointer(bytes,finalize+0x85,command+0x918); + bytes[finalize+0x8d]=1; + word(bytes,finalize+0x94,0x4000_0002); + } + Ok(length) + } +} + +#[cfg(test)] +mod native_tests { + use super::*; + #[test] fn j613_fragment_ends_before_the_m4_tail_and_keeps_relative_restart() { + let fw=|index:u64|FirmwareVa::new(0xffff_fc25_2000_0000+index*0x4000).unwrap(); + let o=Owners {command:Region::new(fw(0).get(),0xca0).unwrap(),sequence:Region::new(fw(1).get(),STORAGE).unwrap(), + stats:Region::new(fw(2).get(),0x1000).unwrap(),notifier:Region::new(fw(3).get(),0x200).unwrap(), + queue:fw(4),scene:fw(5),manager:fw(6),pool:fw(7),fw_stamp:fw(8),context:3,stamp:0x3d000100}; + let mut output=[0xa5;STORAGE+8]; + let length=o.encode_extended(true,super::super::m3_compute_layout::GpuVa::new(0x72_3000_0200).unwrap(),1,&mut output).unwrap(); + assert_eq!(length,0x370); + let word=|at|u32::from_le_bytes(output[at..at+4].try_into().unwrap()); + assert_eq!(word(0x1e4),0x8000_0003); + assert_eq!(word(0x230),1); + assert_eq!(word(0x234),3); + assert_eq!(word(0x280),8); + assert_eq!(word(0x320),(-0x280i32)as u32); + assert_eq!(word(0x334),0x4000_0002); + assert_eq!(&output[STORAGE..],&[0xa5;8]); + } +} + +/// Native start records reserve one packed attachment list per engine. The +/// matching finalizer flag enables completion visibility for that same list. +pub(crate) const fn extended_attachment_offsets(fragment:bool)->(usize,usize) { + if fragment {(0xa8,0x324)} else {(0x88,0x2ec)} +} + +/// Send both pieces through the same bounded writer used for firmware storage. +#[cfg(any(test, CONFIG_DRM_ASAHI_M3_T8122))] +pub(crate) fn patch_extended_attachments(fragment:bool, + attachments:&super::agx_attachments::Attachments, + mut write:impl FnMut(usize,&[u8])->Result<(),E>)->Result<(),E> { + let (start,flag)=extended_attachment_offsets(fragment); + write(start,&attachments.0)?; + write(flag,&[u8::from(attachments.count()!=0)]) +} + +#[cfg(test)] +mod attachment_tests { + use super::*; + use super::super::agx_attachments::{Attachments,SIZE}; + #[test] fn each_engine_keeps_its_list_and_can_clear_a_previous_list() { + for (fragment,start,flag,pool) in [(false,0x88,0x2ec,0x190),(true,0xa8,0x324,0x1b0)] { + let mut bytes=[0xa5;STORAGE]; + let list=Attachments::new(&[(0x100_0040,0x180),(0x200_0000,0x4000)]).unwrap(); + let mut apply=|list:&Attachments|patch_extended_attachments(fragment,list,|at,data|{ + bytes.get_mut(at..at+data.len()).ok_or(Error::Size)?.copy_from_slice(data);Ok::<(),Error>(()) + }); + apply(&list).unwrap(); + assert_eq!(&bytes[start..start+SIZE],&list.0); + assert_eq!(bytes[flag],1); + assert_eq!(&bytes[pool..pool+8],&[0xa5;8]); + patch_extended_attachments(fragment,&Attachments::EMPTY,|at,data|{ + bytes[at..at+data.len()].copy_from_slice(data);Ok::<(),Error>(()) + }).unwrap(); + assert!(bytes[start..start+SIZE].iter().all(|b|*b==0)); + assert_eq!(bytes[flag],0); + assert_eq!(bytes[start-1],0xa5); + assert_eq!(bytes[start+SIZE],0xa5); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_runtime.rs b/drivers/gpu/drm/asahi/m3/m3_runtime.rs index 058c6610399ae3..3b1f3d6d085356 100644 --- a/drivers/gpu/drm/asahi/m3/m3_runtime.rs +++ b/drivers/gpu/drm/asahi/m3/m3_runtime.rs @@ -390,7 +390,7 @@ impl Inner { if found.is_none() { let job=match control { crate::m3::m3_submit::Command::Compute(c)=>NativeJob::Compute(crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,c,self.device.soc().registers,self.device.firmware().profile()).map_err(early)?), - crate::m3::m3_submit::Command::Render{..}=>NativeJob::Render(crate::m3::m3_render::Render::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,self.device.soc().clusters,self.device.soc().registers).map_err(early)?), + crate::m3::m3_submit::Command::Render{..}=>NativeJob::Render(crate::m3::m3_render::Render::new(&self.drm,&self.uat,&packet.vm,self.config.render_stats().map_err(early)?,self.device.soc().clusters,self.device.soc().registers,self.device.firmware().profile()).map_err(early)?), }; self.jobs.push(job,GFP_KERNEL).map_err(|_|early(ENOMEM))?; } else { @@ -439,6 +439,7 @@ impl Inner { let timed=timestamps.map(|pair|policy::stage_timestamps(internal_timestamps,chip,pair)); j.append(command,usc,coalesce,timed)?; j.set_user_timestamps(timestamps)?; + j.set_attachments(&p.render_attachments[*next])?; coalesced_stamps+=u64::from(coalesce); } Ok(()) @@ -563,6 +564,7 @@ impl Inner { let timed=timestamps.map(|pair|policy::stage_timestamps(internal_timestamps,self.device.soc().chip_id,pair)); j.append_after(command,usc,false,predecessor,timed)?; j.set_user_timestamps(timestamps)?; + j.set_attachments(&p.render_attachments[*i])?; dependencies+=usize::from(predecessor.is_some()); previous=Some((true,j.completion_dependency()?)); }, diff --git a/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs b/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs index 880cf4c2b9a75d..ea3c2064ef39e6 100644 --- a/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs @@ -39,3 +39,43 @@ impl Scene { Ok(()) } } + +pub(crate) const EXTENDED_SIZE: usize = 0x400; + +/// Encode a native scene header from its retained aliases. The local +/// statistics and parameter scratch remain part of this same owned record. +pub(crate) fn encode_extended(output: &mut [u8], owner: super::m3_init_layout::Region, + scratch_gpu: super::m3_pool_layout::GpuRegion, scratch_fw: FirmwareVa, + scene_list: super::m3_pool_layout::GpuRegion) -> Result<(), Error> { + let statistics = owner.at(0x200, 0x40).map_err(|_| Error::Size)?; + owner.at(0, EXTENDED_SIZE).map_err(|_| Error::Size)?; + if scratch_gpu.size() < 0x4000 || scene_list.size() < 0x4000 { return Err(Error::Size); } + let compact = super::agx_render_state::compact(scene_list.base()).map_err(|_| Error::Address)?; + let bytes = output.get_mut(..EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..8].copy_from_slice(&scratch_gpu.base().to_le_bytes()); + bytes[8..16].copy_from_slice(&scratch_fw.get().to_le_bytes()); + bytes[0x28..0x30].copy_from_slice(&compact.to_le_bytes()); + bytes[0x40..0x48].copy_from_slice(&statistics.to_le_bytes()); + Ok(()) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + use super::super::{m3_init_layout::Region, m3_pool_layout::GpuRegion}; + #[test] fn native_scene_keeps_scratch_aliases_and_local_statistics_distinct() { + let owner = Region::new(0xffff_fc25_0010_0000, EXTENDED_SIZE).unwrap(); + let scratch = GpuRegion::new(0x10_0200_0000, 0x4000).unwrap(); + let scratch_fw = FirmwareVa::new(0xffff_fc25_0020_0000).unwrap(); + let list = GpuRegion::new(0x10_0300_0000, 0x4000).unwrap(); + let mut bytes = [0xa5; EXTENDED_SIZE + 8]; + encode_extended(&mut bytes, owner, scratch, scratch_fw, list).unwrap(); + let read = |at| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + assert_eq!(read(0), scratch.base()); + assert_eq!(read(8), scratch_fw.get()); + assert_eq!(read(0x28), 0x300_0000); + assert_eq!(read(0x40), owner.at(0x200, 0x40).unwrap()); + assert_eq!(&bytes[EXTENDED_SIZE..], &[0xa5; 8]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs b/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs index 9b8d503836d0a7..a18197890a1ff1 100644 --- a/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs @@ -100,3 +100,26 @@ pub(crate) fn fault_buffers()->impl Iterator { .chain((PB_FIRST..PB_FIRST+PB_GROUPS).map(|i|(i,256))) .chain([(TILEMAP_LABEL,1280),(HEAP_METADATA_LABEL,256)]) } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, index: usize) -> Result { + let mut owner = allocation(index)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + if owner.space == Space::Firmware { + owner.address = 0xffff_fc24_0000_0000 + index as u64 * 0x10_0000; + } + match index { + 8 | 20 => owner.size = 0x20000, + 9 | 21 => { owner.address &= !0x3fff; owner.size = 0x4000; } + 10 | 22 => { + owner.size = 0x4000; + owner.gpu_alias = Some(0x72_3000_0000 + u64::from(index == 22) * 0x4000); + } + GPU_CONTEXT => owner.size = 0x38, + NOTIFIER => owner.size = 0x400, + EVENT_COUNT => owner.size = 16, + BUFFER_MANAGER => owner.size = 0xc0, + FRAGMENT_QUEUE | TA_QUEUE => owner.size = 0x24c0, + _ => {} + } + Ok(owner) +} diff --git a/drivers/gpu/drm/asahi/m3/m3_submit.rs b/drivers/gpu/drm/asahi/m3/m3_submit.rs index c3a25c9a2dc04d..155bda714e9d9f 100644 --- a/drivers/gpu/drm/asahi/m3/m3_submit.rs +++ b/drivers/gpu/drm/asahi/m3/m3_submit.rs @@ -42,6 +42,7 @@ pub(crate) struct Packet { timestamps:KVec<[Option;4]>, pub(crate) attachments:KVec, pub(crate) wide_visibility:KVec, + pub(crate) render_attachments:KVec<[crate::m3::agx_attachments::Attachments;2]>, completion:UserFence, finish_claimed: AtomicBool, /// Set before a batch with any of this packet's commands can change firmware-visible state, @@ -197,7 +198,7 @@ impl queue::Queue for Queue { fn submit(&mut self,_reg_data:&crate::driver::AsahiRegData<'_>,_id:u64,mut syncs:KVec,in_sync_count:usize, raw:&[u8],objects:Pin<&xarray::XArray>>)->Result { let vm_job=self.vm.retain_job()?; - let mut parser=agx_uapi::UapiCommandParser::new(raw);let mut commands=KVec::new();let mut timestamps=KVec::new();let mut flushes=KVec::new();let mut visibility=KVec::new(); + let mut parser=agx_uapi::UapiCommandParser::new(raw);let mut commands=KVec::new();let mut timestamps=KVec::new();let mut flushes=KVec::new();let mut visibility=KVec::new();let mut render_attachments=KVec::new(); while let Some(command)=parser.next_hardware().map_err(|_|EINVAL)? { if let agx_uapi::ParsedHardwareCommand::Render{payload,vertex_attachments,fragment_attachments,..}=command { if let Err(e)=validate_render(payload,self.usc,&AddressSpace(&self.vm),self.early_tiling) { @@ -212,6 +213,7 @@ impl queue::Queue for Queue { Destination::resolve(objects,payload.fragment_timestamps.start)?, Destination::resolve(objects,payload.fragment_timestamps.end)?],GFP_KERNEL)?; flushes.push(crate::m3::agx_attachments::Attachments::EMPTY,GFP_KERNEL)?; + render_attachments.push([encode_attachments(&self.vm,&vertex_attachments)?,encode_attachments(&self.vm,&fragment_attachments)?],GFP_KERNEL)?; visibility.push(false,GFP_KERNEL)?; continue; } @@ -227,6 +229,7 @@ impl queue::Queue for Queue { // The common translator validates the original command/attachment // addresses; this M3 scheduling hint changes none of them. payload.flags &= !wide; + render_attachments.push([crate::m3::agx_attachments::Attachments::EMPTY;2],GFP_KERNEL)?; flushes.push(encode_attachments(&self.vm,&attachments)?,GFP_KERNEL)?; let c=agx_uapi::translate_compute_command(payload,attachments, agx_uapi::QueueUscWindow{base:self.usc,user_start:0x4000,user_end:(1u64<<42)-0x8000}, @@ -240,7 +243,7 @@ impl queue::Queue for Queue { commands.push(Command::Compute(crate::m3::agx_compute::Control{base:c.control_stream_base,end:c.control_stream_end,usc_base:self.usc}),GFP_KERNEL)?; } parser.finish().map_err(|_|EINVAL)?; - let packet=Arc::new(Packet{vm:self.vm.clone(),commands,timestamps,attachments:flushes,wide_visibility:visibility, + let packet=Arc::new(Packet{vm:self.vm.clone(),commands,timestamps,attachments:flushes,wide_visibility:visibility,render_attachments, completion:self.fences.new_fence(0,Completion)?.into(), finish_claimed:AtomicBool::new(false),published:AtomicBool::new(false),urgency:self.urgency,owner:Owner::current(), vm_job:KBox::pin_init(new_mutex!(Some(vm_job)),GFP_KERNEL)?},GFP_KERNEL)?; diff --git a/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs b/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs index 92d533efa82345..2cf5e189140430 100644 --- a/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs @@ -91,3 +91,58 @@ impl Barrier { Ok(()) } } + +impl InitBufferManager { + /// The extended rebuild path requires a zero staged-block count. Appending + /// already-staged blocks would leave a previous hardware descriptor live. + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 || self.slot >= 32 { return Err(Error::Address); } + Self { context: self.context, slot: self.slot, block_count: 0, + manager: self.manager, stamp: self.stamp }.encode(output) + } +} + +impl Barrier { + pub(crate) const EXTENDED_SIZE: usize = 0x40; + + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.event >= 128 || self.wait_value == 0 || self.self_value == 0 { + return Err(Error::Address); + } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + u32_at(bytes, 0, 4); + address_at(bytes, 4, self.stamp); + address_at(bytes, 12, self.stamp); + u32_at(bytes, 0x14, self.wait_value); + u32_at(bytes, 0x20, self.event); + u32_at(bytes, 0x24, self.self_value); + if matches!(self.dependency, Dependency::PreviousPass) { u32_at(bytes, 0x3c, 1); } + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn full_rebuild_never_uses_the_legacy_staged_count() { + let mut bytes = [0xa5; INIT_BM_SIZE]; + InitBufferManager { context: 3, slot: 7, block_count: 2048, + manager: FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), stamp: 0x100 } + .encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[12..20], &[0; 8]); + } + #[test] fn extended_dependencies_use_their_native_tail_and_preserve_trailing_storage() { + for previous in [false, true] { + let mut bytes = [0xa5; Barrier::EXTENDED_SIZE + 8]; + Barrier { stamp: FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), wait_value: 0x100, + event: 2, self_value: 0x200, uuid: 9, + dependency: if previous { Dependency::PreviousPass } else { Dependency::TilerToFragment } } + .encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[0x18..0x20], &[0; 8]); + assert_eq!(&bytes[0x20..0x24], &[2, 0, 0, 0]); + assert_eq!(u32::from_le_bytes(bytes[0x3c..0x40].try_into().unwrap()), previous.into()); + assert_eq!(&bytes[Barrier::EXTENDED_SIZE..], &[0xa5; 8]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs b/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs index eeda857a3ed88a..caa61621e0b049 100644 --- a/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs +++ b/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs @@ -3,6 +3,17 @@ use super::{agx_render::Geometry, m3_compute_layout::{GpuVa,RegisterSet}, m3_init_layout::{Error,Region}, m3_queue_layout::FirmwareVa, m3_render_sequence::ta}; pub(crate) const SIZE:usize=0x92b; +pub(crate) const EXTENDED_SIZE:usize=0x940; +pub(crate) const EXTENDED_FLUSH:usize=0x894; +pub(crate) struct Dispatch26 { + pub(crate) incarnation:u8, + pub(crate) serial:u64, + pub(crate) peer_stamp:u32, + pub(crate) usc_window:u64, + pub(crate) firmware_aux:FirmwareVa, + pub(crate) scene_scratch:GpuVa, +} + pub(crate) const REGISTERS:usize=ta::REGISTERS; /// The registers G15G does not take; the others keep the G15S order. pub(crate) const G15G_OMIT:[u32;14]=[0x1c041,0x1c9d0,0x1c948,0x1c888,0x1c890,0x1c918,0x1c089,0x1c9e0,0x16c41,0x1ca40,0x1c9a8,0x1c920,0xa5a1,0xd419]; @@ -57,17 +68,32 @@ impl Command { /// All address checks precede mutation. Zero all reserved fields, then /// emit semantic registers and explicit packed command/tail fields. pub(crate) fn encode(&self,out:&mut[u8])->Result<(),Error> { - if out.len() Result<(),Error> { + self.encode_format(out, Some(dispatch)) + } + fn encode_format(&self, out: &mut [u8], dispatch: Option) -> Result<(),Error> { + let extended = dispatch.is_some(); + let size = if extended { EXTENDED_SIZE } else { SIZE }; + if out.len()= 1 << 42 || self.register_set != RegisterSet::G15G { + return Err(Error::Address); + } + } if self.context>=64 || self.page_count==0 || self.tpc_bytes==0 {return Err(Error::Address);} let registers=self.gpu_alias.get().checked_add(REGISTERS as u64).ok_or(Error::Address)?; - GpuVa::new(self.gpu_alias.get().checked_add((SIZE-3) as u64).ok_or(Error::Address)?) + GpuVa::new(self.gpu_alias.get().checked_add(((size-1)&!3) as u64).ok_or(Error::Address)?) .map_err(|_|Error::Address)?; - let seq=self.sequence.at(0,ta::LENGTH)?; + let sequence_size = if extended { 0x300 } else { ta::LENGTH }; + let seq=self.sequence.at(0,sequence_size)?; let tilemap=compact(self.tilemap.get())?; let tpc=compact(self.tpc.get())?; let heap=compact(self.heap.get())?; let preemption=[compact(self.preemption[0].get())?,compact(self.preemption[1].get())?,compact(self.preemption[2].get())?]; - let user=compact(self.scene_user.get().checked_add(0x40).ok_or(Error::Address)?)?; + let user=compact(self.scene_user.get().checked_add(if extended {0} else {0x40}).ok_or(Error::Address)?)?; let g=&self.geometry; let layers=g.layers; let layer_metadata=compact(self.heap.get().checked_add(if layers>1 {0x200}else{0}).ok_or(Error::Address)?)?; @@ -99,23 +125,67 @@ impl Command { (0x1c830,0),(0x1ca30,user),(0x16c39,user), (0x1c910,0xa0_00000000|(self.initial_scene_entry.get()&0xf_ffffffff)|1), ]; - let bytes=&mut out[..SIZE];bytes.fill(0); // TA opcode is zero - pointer(bytes,4,1); // qualified work-command counter remains one + let bytes=&mut out[..size];bytes.fill(0); // TA opcode is zero + pointer(bytes,4,dispatch.as_ref().map_or(1, |native| native.serial)); word(bytes,CONTEXT,self.context); - for (offset,value) in [(0x14,self.notifier.get()),(0x24,self.manager.get()), - (0x2c,self.scene.get()),(0x34,self.empty.get()),(0x740,registers), + for (offset,value) in [(if extended {0x10} else {0x14},self.notifier.get()), + (if extended {0x20} else {0x24},self.manager.get()), + (if extended {0x28} else {0x2c},self.scene.get()), + (if extended {0x30} else {0x34},self.empty.get()),(0x740,registers), (0x760,self.tpc.get()),(0x768,self.tpc_bytes),(0x770,seq)] {pointer(bytes,offset,value);} let g15g=self.register_set==RegisterSet::G15G; let mut count=0; - for (register,value) in values.into_iter().filter(|(r,_)| !g15g || !G15G_OMIT.contains(r)) { - let n=count;count+=1; - word(bytes,REGISTERS+n*REGISTER_STRIDE,register); - pointer(bytes,REGISTERS+n*REGISTER_STRIDE+4,value); + let mut emit = |register, value| { + word(bytes, REGISTERS + count * REGISTER_STRIDE, register); + pointer(bytes, REGISTERS + count * REGISTER_STRIDE + 4, value); + count += 1; + }; + for (register, mut value) in values { + if g15g && G15G_OMIT.contains(®ister) { continue; } + if extended && matches!(register, 0x12099 | 0x1c898) { continue; } + if let Some(native) = &dispatch { + if register == 0x101e1 { + emit(0xa309, 0); + emit(0x1c8e0, u64::MAX); + emit(0x1c8e8, u64::MAX); + emit(0x1c898, 0); + } + if register == 0x1c910 { + let address = native.scene_scratch.get(); + let escaped = (address & (1 << 42)) >> 3; + let bias = if escaped == 0 { 0x70_0000_0000 } else { 0 }; + value = ((address + bias) & 0x7f_ffff_fffe) | escaped | 1; + } + } + emit(register, value); + } + if let Some(native) = &dispatch { + emit(0xa5a1, 0x60_0040_0020); + emit(0xd419, 0x2_0000_0001); + for register in [0x1ca10, 0x14a1, 0xa349] { emit(register, ta::UUID.into()); } + emit(0x10061, native.usc_window); } bytes[0x748..0x74a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x74a..0x74c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); - word(bytes,SEQUENCE_LENGTH,ta::LENGTH as u32); + word(bytes,SEQUENCE_LENGTH,sequence_size as u32); word(bytes,0x77c,1); // fragment event slot + if let Some(native) = dispatch { + word(bytes, STAMP_VALUE, native.peer_stamp); + pointer(bytes, 0x7a8, preemption[0]); + pointer(bytes, 0x878, self.stamp.get()); + pointer(bytes, 0x880, self.fw_stamp.get()); + word(bytes, 0x888, self.stamp_value); + word(bytes, EXTENDED_FLUSH, 1); + word(bytes, 0x898, ta::UUID); + pointer(bytes, 0x8c8, self.timestamps[0].get()); + pointer(bytes, 0x8d0, self.timestamps[1].get()); + pointer(bytes, 0x8d8, self.user_timestamps[0].get()); + pointer(bytes, 0x8e0, self.user_timestamps[1].get()); + pointer(bytes, 0x8fe, self.pool.get()); + bytes[0x906] = 1; + pointer(bytes, 0x90f, native.firmware_aux.get()); + bytes[0x917] = native.incarnation; + } else { word(bytes,STAMP_VALUE,self.stamp_value); pointer(bytes,ta::STRUCT3+0x24,preemption[0]); word(bytes,ta::STRUCT3+0xbc,0x67); // retained producer encoder_id @@ -132,6 +202,41 @@ impl Command { (0x47,self.scratch.get())] {pointer(bytes,ta::COMMAND_TAIL+offset,value);} bytes[ta::COMMAND_TAIL+0x36]=1; // unk_846 bytes[ta::COMMAND_TAIL+0x4f]=1; // unk_85f + } Ok(()) } } + +#[cfg(test)] +mod extended_tests { + use super::*; + fn command() -> Command { + let fw=|index:u64|FirmwareVa::new(0xffff_fc25_1000_0000+index*0x4000).unwrap(); + let gpu=|index:u64|GpuVa::new(0x10_0100_0000+index*0x4000).unwrap(); + Command {context:3,register_set:RegisterSet::G15G,notifier:fw(0),manager:fw(1),scene:fw(2),empty:fw(3), + gpu_alias:GpuVa::new(0x72_1000_0000).unwrap(),sequence:Region::new(fw(4).get(),0x1000).unwrap(), + pool:fw(5),scratch:fw(6),stamp:fw(7),fw_stamp:fw(8),stamp_value:0x7a000100, + timestamps:[fw(9),fw(10)],user_timestamps:[fw(11),fw(12)],preemption:[gpu(0),gpu(1),gpu(2)], + tilemap:gpu(3),tpc:gpu(4),tpc_bytes:0x4000,heap:gpu(5),scene_user:gpu(6), + initial_scene_entry:fw(13),geometry:Geometry::new(64,64,super::super::agx_render::Utile::Pixels32, + super::super::agx_render::Utile::Pixels32,1).unwrap(),page_count:128, + vdm:0x100000,multisample:0,ppp:0,process_empty_tiles:false} + } + #[test] fn native_tiler_keeps_peer_completion_and_usc_window_explicit() { + let value=command();let mut bytes=[0xa5;EXTENDED_SIZE+8]; + value.encode_extended(Dispatch26 {incarnation:9,serial:2,peer_stamp:0x3d000100, + usc_window:1<<32,firmware_aux:value.scratch, + scene_scratch:GpuVa::new(0x10_0200_0000).unwrap()},&mut bytes).unwrap(); + let word=|at|u32::from_le_bytes(bytes[at..at+4].try_into().unwrap()); + assert_eq!(word(0x780),0x3d000100); + assert_eq!(word(0x888),value.stamp_value); + assert_eq!(word(0x894),1); + assert_eq!(word(0x748)&0xffff,66); + assert_eq!(bytes[0x917],9); + assert_eq!(&bytes[EXTENDED_SIZE..],&[0xa5;8]); + let registers=&bytes[REGISTERS..REGISTERS+66*REGISTER_STRIDE]; + assert!(!registers.chunks_exact(12).any(|r|u32::from_le_bytes(r[..4].try_into().unwrap())==0x12099)); + let usc=registers.chunks_exact(12).find(|r|u32::from_le_bytes(r[..4].try_into().unwrap())==0x10061).unwrap(); + assert_eq!(u64::from_le_bytes(usc[4..].try_into().unwrap()),1<<32); + } +} diff --git a/drivers/gpu/drm/asahi/m3/t8122_admission.rs b/drivers/gpu/drm/asahi/m3/t8122_admission.rs index c66ccb72cd18d7..65a1df4bc0a6b6 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_admission.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_admission.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! Read-only T8122 identity admission. Firmware startup remains disabled. +//! Read-only T8122 identity and firmware-interface admission. use super::m3_profile::Profile; @@ -53,6 +53,28 @@ pub(crate) fn validate_identity_for( }) } +/// Firmware-interface selection is exact and does not change physical GPU encodings. +pub(crate) fn profile_for(compat:&[u32],j613:bool)->Option { + match compat { + [14,8,3]=>Some(Profile::Legacy), + [26,6,2] if j613=>Some(Profile::Extended), + _=>None, + } +} + +#[cfg(not(test))] +pub(crate) fn loaded_profile(pdev:&kernel::platform::Device>) + ->kernel::error::Result { + use kernel::{c_str,prelude::*}; + let node=pdev.as_ref().of_node().ok_or(ENODEV)?; + let compat:KVec=node.get_property(c_str!("apple,firmware-compat"))?; + #[cfg(CONFIG_DRM_ASAHI_M3_T8122)] + let j613 = super::m3_board::board_is(b"apple,j613"); + #[cfg(not(CONFIG_DRM_ASAHI_M3_T8122))] + let j613 = false; + profile_for(&compat, j613).ok_or(ENODEV) +} + /// Shape of the voltage-sorted running-state tables: one entry per distinct millivolt, /// and the highest primary frequency in MHz. Voltages round up, matching `PwrConfig`. /// The first point is the off state. Both primary and secondary frequencies must increase @@ -108,7 +130,7 @@ pub(crate) fn opp_table_shape(opps: impl IntoIterator) -> Opt pub(crate) fn read_identity( pdev: &kernel::platform::Device>, ) -> kernel::error::Result { - read_identity_for(pdev, Profile::Legacy) + read_identity_for(pdev, loaded_profile(pdev)?) } #[cfg(not(test))] @@ -300,3 +322,17 @@ mod tests { assert!(validate_identity(&[14, 8, 3], 1, 1, G15G_C0, [0x7f, 0, 0]).is_none()); } } + +#[cfg(test)] +mod selection_tests { + use super::*; + #[test] fn extended_admission_requires_exact_firmware_and_j613() { + for board in [false,true] { + assert_eq!(profile_for(&[14,8,3],board),Some(Profile::Legacy)); + assert_eq!(profile_for(&[26,6,2],board),if board{Some(Profile::Extended)}else{None}); + for wrong in [&[][..],&[26,6][..],&[26,6,1][..],&[26,6,2,0][..],&[14,8,4][..]] { + assert_eq!(profile_for(wrong,board),None); + } + } + } +} diff --git a/tools/asahi/m3-runtime-tests.rs b/tools/asahi/m3-runtime-tests.rs index 458603366c954b..713c82f42250fa 100644 --- a/tools/asahi/m3-runtime-tests.rs +++ b/tools/asahi/m3-runtime-tests.rs @@ -46,3 +46,39 @@ mod m3_state_layout; #[path = "../../drivers/gpu/drm/asahi/m3/m3_compute_storage.rs"] mod m3_compute_storage; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs"] +mod m3_parameter_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_sync_layout.rs"] +mod m3_sync_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/agx_render.rs"] +mod agx_render; + +#[path = "../../drivers/gpu/drm/asahi/m3/agx_render_state.rs"] +mod agx_render_state; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_scene_layout.rs"] +mod m3_scene_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_command_layout.rs"] +mod m3_command_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_render_sequence.rs"] +mod m3_render_sequence; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_tiler_command.rs"] +mod m3_tiler_command; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_pass_layout.rs"] +mod m3_pass_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_shared_layout.rs"] +mod m3_shared_layout; + +#[path = "../../drivers/gpu/drm/asahi/m3/m3_fragment_command.rs"] +mod m3_fragment_command; + +#[path = "../../drivers/gpu/drm/asahi/m3/agx_attachments.rs"] +mod agx_attachments; From 98bca8bff9b67abdfe9ce98ebad7d1c54a545a79 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:34:30 -0500 Subject: [PATCH 09/15] soc: apple: rtkit: allow receivers to observe mailbox delivery time Add optional timestamped delivery while preserving the ordinary C and Rust receiver paths. The native display readiness check needs the receipt time rather than deferred callback execution time. Signed-off-by: Satya Benson --- drivers/gpu/drm/asahi/g15/boot.rs | 1 + drivers/soc/apple/rtkit.c | 9 ++++++++- include/linux/soc/apple/rtkit.h | 4 ++++ rust/kernel/soc/apple/rtkit.rs | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index 45e244531323d7..4e484790347329 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -317,6 +317,7 @@ static RTKIT_OPS: bindings::apple_rtkit_ops = bindings::apple_rtkit_ops { flags: 0, crashed: Some(rtkit_crashed), recv_message: Some(rtkit_recv), + recv_message_timed: None, recv_message_early: Some(rtkit_recv_early), shmem_setup: Some(rtkit_shmem_setup), shmem_destroy: Some(rtkit_shmem_destroy), diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 11334b3f68faa5..b9d801ed56979d 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -6,6 +6,7 @@ #include "rtkit-internal.h" +#include #include #include #include @@ -108,6 +109,7 @@ struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; + u64 received_ns; struct work_struct work; }; @@ -832,7 +834,10 @@ static void apple_rtkit_rx_work(struct work_struct *work) apple_rtkit_oslog_rx(rtk, rtk_work->msg); break; case APPLE_RTKIT_APP_ENDPOINT_START ... 0xff: - if (rtk->ops->recv_message) + if (rtk->ops->recv_message_timed) + rtk->ops->recv_message_timed(rtk->cookie, rtk_work->ep, + rtk_work->msg, rtk_work->received_ns); + else if (rtk->ops->recv_message) rtk->ops->recv_message(rtk->cookie, rtk_work->ep, rtk_work->msg); else @@ -889,6 +894,7 @@ static void apple_rtkit_rx(struct apple_mbox *mbox, struct apple_mbox_msg msg, struct apple_rtkit *rtk = cookie; struct apple_rtkit_rx_work *work; u8 ep = msg.msg1; + u64 received_ns = ktime_get_ns(); if (READ_ONCE(rtk->shutting_down)) return; @@ -938,6 +944,7 @@ static void apple_rtkit_rx(struct apple_mbox *mbox, struct apple_mbox_msg msg, work->rtk = rtk; work->ep = ep; work->msg = msg.msg0; + work->received_ns = received_ns; INIT_WORK(&work->work, apple_rtkit_rx_work); queue_work(rtk->wq, &work->work); } diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 1ba27a92e2b144..0a2718bc39401d 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -57,6 +57,9 @@ struct apple_rtkit_shmem { * context. * @recv_message: Function called when a message from RTKit is received * on a non-system endpoint. Called from a worker thread. + * @recv_message_timed: Like recv_message, carrying the monotonic nanosecond + * timestamp captured before deferring mailbox RX. If set, + * this callback is used instead of recv_message. * @recv_message_early: * Like recv_message, but called from atomic context. It * should return true if it handled the message. If it @@ -77,6 +80,7 @@ struct apple_rtkit_ops { unsigned int flags; void (*crashed)(void *cookie, const void *crashlog, size_t crashlog_size); void (*recv_message)(void *cookie, u8 endpoint, u64 message); + void (*recv_message_timed)(void *cookie, u8 endpoint, u64 message, u64 received_ns); bool (*recv_message_early)(void *cookie, u8 endpoint, u64 message); int (*shmem_setup)(void *cookie, struct apple_rtkit_shmem *bfr); void (*shmem_destroy)(void *cookie, struct apple_rtkit_shmem *bfr); diff --git a/rust/kernel/soc/apple/rtkit.rs b/rust/kernel/soc/apple/rtkit.rs index 87ec3bd73658bb..2581e44d592fc6 100644 --- a/rust/kernel/soc/apple/rtkit.rs +++ b/rust/kernel/soc/apple/rtkit.rs @@ -224,6 +224,7 @@ impl RtKit { }, crashed: Some(crashed_callback::), recv_message: Some(recv_message_callback::), + recv_message_timed: None, recv_message_early: Some(recv_message_early_callback::), shmem_setup: if T::HAS_SHMEM_ALLOC || T::HAS_SHMEM_MAP { Some(shmem_setup_callback::) From 4fa282ac12b87d32489353095dfefcc88a47c661 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:34:30 -0500 Subject: [PATCH 10/15] drm/apple: acquire the J613 native26 external power suppliers Gate native26 admission on the live handoff and native PMP graph. Update existing supplier properties transactionally so failure restores the previous device tree. Keep the classic and internal display paths separate. Signed-off-by: Satya Benson --- .../bindings/display/apple,dcp.yaml | 34 +++ .../boot/dts/apple/t8122-j613-dcp-25g83.dtsi | 19 +- drivers/gpu/drm/apple/j613-25g83.h | 7 + drivers/gpu/drm/apple/j613_dcp_activate.c | 196 ++++++++++++++++++ drivers/pmdomain/apple/pmp-report.c | 144 ++++++++++++- 5 files changed, 392 insertions(+), 8 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index a7c74d948e6e9c..09df19ef1b3e6b 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -23,6 +23,7 @@ properties: - apple,t8112-dcp - apple,t6000-dcp - apple,t6020-dcp + - apple,t8122-dcp - apple,t8140-dcp - apple,t8142-dcp - const: apple,dcp @@ -32,6 +33,7 @@ properties: - apple,t8112-dcpext - apple,t6000-dcpext - apple,t6020-dcpext + - apple,t8122-dcpext - const: apple,dcpext - items: - const: apple,t8140-dcpext @@ -180,6 +182,38 @@ properties: maxItems: 5 description: Loader-provided firmware ABI version used for IOMFB selection. + apple,j613-25g83-dcpext-handoff: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 1 + description: | + Loader validation of the J613 native26 external processor. Requires + the separately validated internal mapping handoff, exact 26.6.2 image, + complete stream-5 mappings and retained locked page tables. Does not + enable hardware. Firmware reservations must also be complete. + + apple,j613-25g83-dcpext-boot-state: + $ref: /schemas/types.yaml#/definitions/uint32-array + minItems: 4 + maxItems: 4 + description: | + Loader observations in order of powered ancestry, ASC CPU control, + ASC CPU status and firmware-DART protection. Read only while the + processor's full power ancestry is active. A powered processor must + have protected inherited roots; this is not an RTKit readiness claim. + + apple,t8122-dcpext-firmware-ready: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 1 + description: All external firmware reservations were committed by the loader. + + apple,firmware-uuid: + $ref: /schemas/types.yaml#/definitions/string + description: UUID of the actual loaded display image, read from its iop nub. + + apple,pmp: + $ref: /schemas/types.yaml#/definitions/phandle + description: Native PMP supplier, independent of the report request entry. + apple,connector-type: enum: [HDMI-A, DP, USB-C] diff --git a/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi b/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi index e292dda82f0227..57bb0b13bf614a 100644 --- a/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi +++ b/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi @@ -20,7 +20,8 @@ }; &dcpext { - /delete-property/ apple,pmp-report; + apple,pmp-report = <&pmp_report_dispext>; + apple,pmp = <&pmp>; }; &disp0_piodma { @@ -37,10 +38,17 @@ /* This profile requests only DISP and ANS until external admission succeeds. */ &pmp_report_dispext { + apple,soc-device-record; status = "disabled"; }; +&pmp_report_disp { + apple,soc-device-record; +}; + &pmp_report_ans { + apple,soc-device-record; + apple,no-ack; apple,always-on; }; @@ -89,3 +97,12 @@ apple,pmp = <&pmp>; status = "disabled"; }; + +/* Native26 firmware and PIODMA use the same 1-TiB alias as the panel. */ +&dcpext_dart { + apple,dma-range = /bits/ 64 <0x10000000000 0x1000000000>; +}; + +&dispext0_dart { + apple,dma-range = /bits/ 64 <0x10000000000 0x1000000000>; +}; diff --git a/drivers/gpu/drm/apple/j613-25g83.h b/drivers/gpu/drm/apple/j613-25g83.h index 2ac282ce95456f..ead1ef0f9b410e 100644 --- a/drivers/gpu/drm/apple/j613-25g83.h +++ b/drivers/gpu/drm/apple/j613-25g83.h @@ -43,6 +43,13 @@ static inline bool j613_25g83_mappings(const u32 *markers, size_t cells) markers[2] == 1; } +/* This profile adopts a powered RUN session through a locked firmware DART. */ +static inline bool j613_25g83_external_state(const u32 *state, size_t cells) +{ + return state && cells == 4 && state[0] == 1 && + (state[1] & BIT(4)) && (state[3] & BIT(0)); +} + /* * Local copies of the PMP table checks of * drivers/pmdomain/apple/pmp-report-validation.h, which is not in this tree diff --git a/drivers/gpu/drm/apple/j613_dcp_activate.c b/drivers/gpu/drm/apple/j613_dcp_activate.c index 6d0a2620694692..a5c668b5ec4b85 100644 --- a/drivers/gpu/drm/apple/j613_dcp_activate.c +++ b/drivers/gpu/drm/apple/j613_dcp_activate.c @@ -294,6 +294,196 @@ static void j613_resolve(struct device_node **nodes) "apple,t8122-asc-mailbox"); } +static struct of_changeset external_gate; +static bool external_gate_applied; + +/* + * UPDATE_PROPERTY also inserts an absent property and records its old value + * for changeset rollback. Firmware DTs may already constrain these suppliers. + */ +static int __init j613_external_update(struct device_node *np, const char *name, + const void *value, unsigned int length) +{ + struct property *prop = kzalloc_obj(*prop); + int ret; + + if (!prop) + return -ENOMEM; + prop->name = kstrdup(name, GFP_KERNEL); + prop->length = length; + if (length) + prop->value = kmemdup(value, length, GFP_KERNEL); + if (!prop->name || (length && !prop->value)) { + ret = -ENOMEM; + goto free; + } + ret = of_changeset_update_property(&external_gate, np, prop); + if (!ret) + return 0; +free: + kfree(prop->value); + kfree(prop->name); + kfree(prop); + return ret; +} + + +/* Optional external transaction; its failure must not unwind the panel. */ +static int __init j613_external_gate(struct device_node *display) +{ + struct device_node *chosen, *ext = NULL, *pio = NULL, *report = NULL; + struct device_node *nodes[9] = {}, *cpu = NULL, *routes = NULL, *route; + struct of_phandle_args args; + const char *opt, *uuid, *status, *loader_result; + const char *reason = "external node or compatible"; + struct property *prop = NULL; + const void *old; + __be32 *cells; + u32 state[4], version[3], ready; + int len, ret = -EINVAL, i, count = 0; + + chosen = of_find_node_by_path("/chosen"); + opt = chosen ? of_get_property(chosen, "asahi,j613-25g83-dcpext", &len) : NULL; + if (!opt || len != 2 || opt[0] != '1' || opt[1]) { + of_node_put(chosen); + return 0; + } + loader_result = of_get_property(chosen, "asahi,j613-25g83-dcpext-result", &len); + if (loader_result && len > 0 && loader_result[len - 1] == 0) + pr_info("J613 native26 external loader result: %s\n", loader_result); + else + pr_info("J613 native26 external loader result: absent or malformed\n"); + of_node_put(chosen); + if (!of_machine_is_compatible("apple,j613")) + return -ENODEV; + ext = of_find_node_by_path("dcpext"); + if (!ext) + ext = of_find_node_by_path("dcpext0"); +#define J613_EXT_REQUIRE(test, why) do { \ + if (!(test)) { \ + reason = why; \ + goto out; \ + } \ +} while (0) + J613_EXT_REQUIRE(ext && of_device_is_compatible(ext, "apple,t8122-dcpext"), + "external node or compatible"); + J613_EXT_REQUIRE(of_property_count_u32_elems(ext, "apple,firmware-compat") == 3 && + !of_property_read_u32_array(ext, "apple,firmware-compat", version, 3), + "missing or malformed apple,firmware-compat"); + J613_EXT_REQUIRE(!of_property_read_string(ext, "apple,firmware-uuid", &uuid), + "missing apple,firmware-uuid"); + J613_EXT_REQUIRE(j613_25g83_identity(true, 1, version, 3, uuid), "firmware identity"); + J613_EXT_REQUIRE(!of_property_read_u32(ext, "apple,j613-25g83-dcpext-handoff", &ready) && ready == 1, + "missing native handoff marker"); + J613_EXT_REQUIRE(!of_property_read_u32(ext, "apple,t8122-dcpext-firmware-ready", &ready) && ready == 1, + "missing firmware-ready marker"); + J613_EXT_REQUIRE(of_property_count_u32_elems(ext, "apple,j613-25g83-dcpext-boot-state") == 4 && + !of_property_read_u32_array(ext, "apple,j613-25g83-dcpext-boot-state", state, 4), + "missing or malformed boot-state"); + J613_EXT_REQUIRE(j613_25g83_external_state(state, 4), "boot-state not powered RUN and locked"); + J613_EXT_REQUIRE(of_property_present(ext, "memory-region"), "missing memory-region"); +#undef J613_EXT_REQUIRE + pr_info("J613 native26 external handoff: identity, markers, boot-state and memory-region accepted\n"); + reason = "PMP/DART/mailbox suppliers"; + nodes[count++] = of_node_get(ext); + nodes[count++] = j613_target(ext, "iommus", "#iommu-cells", 5, "apple,t8122-dart"); + nodes[count++] = j613_target(ext, "mboxes", "#mbox-cells", -1, "apple,t8122-asc-mailbox"); + pio = of_get_child_by_name(ext, "piodma"); + nodes[count++] = pio ? j613_target(pio, "iommus", "#iommu-cells", 4, "apple,t8122-dart") : NULL; + nodes[count++] = j613_target(ext, "apple,pmp-report", NULL, -1, + "apple,t6000-pmp-v2-report-entry"); + if (!nodes[4] || of_property_read_u32(nodes[4], "reg", &ready) || ready != 8 || + of_property_read_bool(nodes[4], "apple,no-ack") || !nodes[1] || + !nodes[3] || nodes[1] == nodes[3]) + goto out; + reason = "PMP report compatible"; + report = of_get_parent(nodes[4]); + if (!report || !of_device_is_compatible(report, "apple,j613-25g83-pmp-report")) + goto out; + reason = "DISPEXT CPU power domain"; + cpu = of_parse_phandle(ext, "power-domains", 0); + if (!cpu || of_property_read_string(cpu, "label", &status) || strcmp(status, "dispext_cpu") || + !of_property_read_bool(cpu, "apple,firmware-owned")) + goto out; + reason = "two crossbar routes"; + if (of_count_phandle_with_args(ext, "mux-controls", "#mux-control-cells") != 2) + goto out; + for (i = 0; i < 2; i++) { + if (of_parse_phandle_with_args(ext, "mux-controls", "#mux-control-cells", i, &args)) + goto out; + nodes[count++] = args.np; + if (args.args_count != 1 || args.args[0] || + !of_device_is_compatible(args.np, "apple,t6020-display-crossbar")) + goto out; + } + reason = "supplier must be disabled"; + for (i = 0; i < count; i++) { + if (!nodes[i] || of_property_read_string(nodes[i], "status", &status) || + strcmp(status, "disabled")) + goto out; + } + reason = "two Type-C route children"; + routes = of_get_child_by_name(ext, "typec-routes"); + if (of_get_child_count(routes) != 2) + goto out; + reason = "internal display IOMMU shape"; + old = of_get_property(display, "iommus", &len); + if (!old || len != 8 || !nodes[3]->phandle) + goto out; + reason = "changeset allocation"; + prop = kzalloc_obj(*prop); + cells = kmalloc(16, GFP_KERNEL); + if (!prop || !cells) { + kfree(prop); + kfree(cells); + prop = NULL; + ret = -ENOMEM; + goto out; + } + memcpy(cells, old, 8); + cells[2] = cpu_to_be32(nodes[3]->phandle); + cells[3] = 0; + prop->name = kstrdup("iommus", GFP_KERNEL); + prop->length = 16; + prop->value = cells; + if (!prop->name) { + kfree(cells); + kfree(prop); + ret = -ENOMEM; + goto out; + } + reason = "resource changeset"; + of_changeset_init(&external_gate); + ret = of_changeset_update_property(&external_gate, display, prop); + if (!ret) + ret = j613_external_update(nodes[4], "apple,always-on", NULL, 0); + for (i = 0; i < count && !ret; i++) + ret = of_changeset_update_prop_string(&external_gate, nodes[i], "status", "okay"); + for_each_child_of_node(routes, route) { + if (!ret) + ret = of_changeset_update_prop_string(&external_gate, route, "status", "okay"); + } + if (!ret) + ret = of_changeset_apply(&external_gate); + if (ret) + of_changeset_destroy(&external_gate); + else { + external_gate_applied = true; + pr_info("J613 native26 external gate accepted: reserved firmware, DISPEXT12 and both Type-C routes\n"); + } +out: + if (ret) + pr_info("J613 native26 external gate refused: %d (%s); internal display retained\n", ret, reason); + for (i = 0; i < count; i++) + of_node_put(nodes[i]); + of_node_put(cpu); + of_node_put(routes); + of_node_put(report); + of_node_put(pio); + of_node_put(ext); + return ret; +} + int __init apple_j613_25g83_coldplug(void) { struct device_node *nodes[J613_NR_NODES] = {}, *chosen; @@ -389,11 +579,17 @@ int __init apple_j613_25g83_coldplug(void) for (i = 0; i < ARRAY_SIZE(nodes) && !ret; i++) ret = of_changeset_update_prop_string(&gate, nodes[i], "status", "okay"); if (!ret) { + j613_external_gate(nodes[J613_DISPLAY]); display_clock_hz = witness[3]; ret = of_changeset_apply(&gate); } if (ret) { display_clock_hz = 0; + if (external_gate_applied) { + of_changeset_revert(&external_gate); + of_changeset_destroy(&external_gate); + external_gate_applied = false; + } of_changeset_destroy(&gate); } else { display_clock_hz = witness[3]; diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 9aac3ced17cda1..f76c1773432694 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -77,6 +77,8 @@ struct apple_pmp_report { struct dentry *debugfs; /* Set only for a report whose halted PMP this driver starts. */ struct apple_pmp_start *start; + struct device_node *native_pmp; + u64 native_claimed; }; static const struct apple_pmp_report_offsets apple_pmp_offsets_t6030; @@ -175,6 +177,7 @@ static void apple_pmp_report_release(void *data) synchronize_rcu(); /* The debugfs proxy drains active reads and rejects subsequent accesses. */ debugfs_remove(rep->debugfs); + of_node_put(rep->native_pmp); } /* The unique, enabled and acknowledged T6030 display request (DISP, reg 7). */ @@ -449,7 +452,7 @@ static const struct { * records are not in ID order, so the bit is the index of the record that * holds that ID. Only a report that starts its PMP has that table. */ -static int apple_pmp_report_bit(const struct apple_pmp_start *st, +static int apple_pmp_report_record(struct device_node *pmp, struct device_node *entry, u32 *bit, bool *record) { const u8 *table; @@ -463,9 +466,9 @@ static int apple_pmp_report_bit(const struct apple_pmp_start *st, *bit = reg; return 0; } - if (!st) + if (!pmp) return -EINVAL; - table = of_get_property(st->pmp, "apple,tunable-soc-device", &len); + table = of_get_property(pmp, "apple,tunable-soc-device", &len); index = apple_pmp_record_of_id(table, len > 0 ? len : 0, reg + 1); if (index < 0 || index > 63) return -ENOENT; @@ -473,6 +476,73 @@ static int apple_pmp_report_bit(const struct apple_pmp_start *st, return 0; } +static int apple_pmp_report_bit(const struct apple_pmp_start *st, + struct device_node *entry, u32 *bit, bool *record) +{ + return apple_pmp_report_record(st ? st->pmp : NULL, entry, bit, record); +} + +/* The native PMP owns startup; this report still exclusively owns PTD votes. */ +static int apple_pmp_native_records(struct apple_pmp_report *rep) +{ + struct device_node *pmp, *link; + const u8 *table, *group, *ranges; + const char *uuid; + int ntable, ngroup, nranges; + + { + struct device_node *chosen = of_find_node_by_path("/chosen"); + const char *opt = NULL; + int len = 0; + bool selected; + + if (chosen) + opt = of_get_property(chosen, "asahi,j613-25g83-dcpext", &len); + selected = opt && len == 2 && opt[0] == '1' && !opt[1] && + of_device_is_compatible(rep->dev->of_node, "apple,j613-25g83-pmp-report"); + of_node_put(chosen); + if (!selected) + return 0; + } + if (!of_machine_is_compatible("apple,j613") || + !of_device_is_compatible(rep->dev->of_node, "apple,j613-25g83-pmp-report")) + return -ENODEV; + for_each_compatible_node(pmp, NULL, "apple,t8122-pmp-v2") { + link = of_parse_phandle(pmp, "apple,pmp-report", 0); + if (link != rep->dev->of_node || !of_device_is_available(pmp)) { + of_node_put(link); + continue; + } + of_node_put(link); + if (rep->native_pmp) { + of_node_put(pmp); + return -EINVAL; + } + rep->native_pmp = of_node_get(pmp); + } + pmp = rep->native_pmp; + if (!pmp || of_property_read_string(pmp, "apple,tunable-uuid", &uuid) || + strcmp(uuid, "EFD60284-7C58-33A9-8522-CB3366AF6040")) + return -EINVAL; + table = of_get_property(pmp, "apple,tunable-soc-device", &ntable); + group = of_get_property(pmp, "apple,tunable-soc-device-ps-group", &ngroup); + ranges = of_get_property(pmp, "apple,tunable-ptd-range", &nranges); + if (!table || ntable != 20 * PMP_SOC_DEVICE_SIZE || !group || ngroup != 8 || + get_unaligned_le64(group) != 0x3fff0 || + !apple_pmp_ranges_valid(ranges, nranges, 0x1000, 0x10800, 0x1080) || + apple_pmp_record_of_id(table, ntable, 8) != 11 || + apple_pmp_record_of_id(table, ntable, 9) != 12 || + apple_pmp_record_of_id(table, ntable, 18) != 19 || + memcmp(table + 11 * PMP_SOC_DEVICE_SIZE + 0x74, "DISP\0\0\0\0", 8) || + memcmp(table + 12 * PMP_SOC_DEVICE_SIZE + 0x74, "DISPEXT\0", 8) || + memcmp(table + 19 * PMP_SOC_DEVICE_SIZE + 0x74, "ANS\0\0\0\0\0", 8) || + !apple_pmp_records_valid(table, ntable, BIT_ULL(11) | BIT_ULL(12) | BIT_ULL(19), + BIT_ULL(11) | BIT_ULL(12))) + return -EINVAL; + dev_dbg(rep->dev, "native26 PTD admitted: DISP11 DISPEXT12 ANS19 (no ANS ack)\n"); + return 0; +} + /* The single PMGR power state of this SoC with @label. */ static struct device_node *apple_pmp_start_domain(struct apple_pmp_report *rep, const char *label) @@ -1083,6 +1153,17 @@ int apple_pmp_report_wait_ready(struct device_node *entry, unsigned long timeout list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { if (rep->dev->of_node == parent) { bound = true; + if (rep->native_pmp) { + ret = apple_pmp_report_record(rep->native_pmp, entry, &id, &record); + if (!ret && (!record || id != 12)) + ret = -EINVAL; + if (!ret && (!(readq(rep->base + rep->offsets->status) & 1) || + !(readq(rep->base + rep->offsets->actual) & BIT_ULL(id)) || + !(readq(rep->base + rep->offsets->tgt_read) & BIT_ULL(id)))) + ret = -EIO; + rcu_read_unlock(); + goto out; + } break; } } @@ -1122,6 +1203,10 @@ static int apple_pmp_report_probe(struct platform_device *pdev) if (ret) return ret; + ret = apple_pmp_native_records(rep); + if (ret) + return dev_err_probe(dev, ret, "native26 PTD table refused\n"); + /* Before the entries exist: they request by soc-device record through it. */ if (rep->offsets->starts_pmp) { ret = apple_pmp_start_init(rep); @@ -1265,6 +1350,7 @@ struct apple_pmp_report_entry { bool sleep_release; bool released; bool removed; + struct apple_pmp_report *native_owner; }; #define genpd_to_apple_pmp_report_entry(_genpd) \ @@ -1275,6 +1361,7 @@ static int apple_pmp_report_set_state(struct apple_pmp_report *rep, u32 id, { u64 bit_val = BIT_ULL(id); u64 val; + bool latched = true; unsigned long flags; spin_lock_irqsave(&rep->lock, flags); @@ -1283,7 +1370,11 @@ static int apple_pmp_report_set_state(struct apple_pmp_report *rep, u32 id, if (enable) val |= bit_val; writeq(val, rep->base + rep->offsets->tgt_write); + if (rep->native_pmp) + latched = !!(readq(rep->base + rep->offsets->tgt_read) & bit_val) == enable; spin_unlock_irqrestore(&rep->lock, flags); + if (!latched) + return -EIO; val = readq(rep->base + rep->offsets->status); if ((val & PMP_REPORT_READY) == 0 || no_ack) return 0; @@ -1457,6 +1548,14 @@ static int apple_pmp_report_entry_power_off(struct generic_pm_domain *genpd) static void apple_pmp_report_entry_free(struct apple_pmp_report_entry *ent) { + if (ent->native_owner) { + unsigned long flags; + + spin_lock_irqsave(&ent->native_owner->lock, flags); + ent->native_owner->native_claimed &= ~BIT_ULL(ent->id); + spin_unlock_irqrestore(&ent->native_owner->lock, flags); + } + of_node_put(ent->report_node); of_node_put(ent->node); kfree(ent->genpd.name); @@ -1482,7 +1581,7 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) struct device *dev = &pdev->dev; struct device_node *node = dev->of_node; struct apple_pmp_report_entry *ent; - const struct apple_pmp_report *rep; + struct apple_pmp_report *rep; struct apple_pmp_start *start; int ret; const char *name; @@ -1507,7 +1606,30 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) /* Only a report that starts its PMP requests by record, or without ack. */ rep = dev->parent ? dev_get_drvdata(dev->parent) : NULL; start = rep ? rep->start : NULL; - if (start) { + if (rep && rep->native_pmp) { + unsigned long flags; + bool record; + + ret = apple_pmp_report_record(rep->native_pmp, node, &ent->id, &record); + if (ret || !record || (ent->id != 11 && ent->id != 12 && ent->id != 19)) { + ret = -EINVAL; + goto err_free; + } + ent->no_ack = of_property_read_bool(node, "apple,no-ack"); + if (ent->no_ack != (ent->id == 19)) { + ret = -EINVAL; + goto err_free; + } + spin_lock_irqsave(&rep->lock, flags); + ret = rep->native_claimed & BIT_ULL(ent->id) ? -EBUSY : 0; + if (!ret) { + rep->native_claimed |= BIT_ULL(ent->id); + ent->native_owner = rep; + } + spin_unlock_irqrestore(&rep->lock, flags); + if (ret) + goto err_free; + } else if (start) { bool record; ret = apple_pmp_report_bit(start, node, &ent->id, &record); @@ -1532,13 +1654,21 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) if (of_property_read_bool(node, "apple,always-on")) { ent->genpd.flags |= GENPD_FLAG_ACTIVE_WAKEUP; - apple_pmp_report_entry_set_state(&ent->genpd, true); + ret = apple_pmp_report_entry_set_state(&ent->genpd, true); + if (ret && ent->native_owner) + goto err_free; + /* Native DCPEXT keeps its resident producer and request through s2idle. */ + if (ent->native_owner && ent->id == 12) { + ent->genpd.flags |= GENPD_FLAG_ALWAYS_ON; + ent->sleep_release = false; + } } ent->genpd.power_on = apple_pmp_report_entry_power_on; ent->genpd.power_off = apple_pmp_report_entry_power_off; - ret = pm_genpd_init(&ent->genpd, NULL, true); + ret = pm_genpd_init(&ent->genpd, NULL, + !(ent->genpd.flags & GENPD_FLAG_ALWAYS_ON)); if (ret) { ret = dev_err_probe(dev, ret, "pm_genpd_init failed\n"); goto err_free; From 056547dcabd32c7364f306412d062cbe2f593c29 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:34:31 -0500 Subject: [PATCH 11/15] drm/apple: encode J613 native26 external primary surfaces Validate backing size, dimensions and address arithmetic before constructing the separate 26.6.2 wire record. Initialize nullable layers and decode each supported reply layout explicitly. Signed-off-by: Satya Benson --- drivers/gpu/drm/apple/dcpext26-wire.h | 122 ++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 drivers/gpu/drm/apple/dcpext26-wire.h diff --git a/drivers/gpu/drm/apple/dcpext26-wire.h b/drivers/gpu/drm/apple/dcpext26-wire.h new file mode 100644 index 00000000000000..2fa54054260d3f --- /dev/null +++ b/drivers/gpu/drm/apple/dcpext26-wire.h @@ -0,0 +1,122 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCPEXT26_WIRE_H +#define APPLE_DCPEXT26_WIRE_H + +#include +#include +#include +#include + +/* J613 26.6.2 external personality; neither classic 14.7 nor H17P. */ +#define DCPEXT26_SUBMIT_BYTES 0x1bd8 +#define DCPEXT26_SURFACE_BYTES 0x22c + +struct dcpext26_primary { + u64 address; + u64 backing_bytes; + u32 width; + u32 height; + u32 pitch; +}; + +/* Validate all arithmetic before changing a caller-owned command buffer. */ +static inline int dcpext26_primary_check(const struct dcpext26_primary *p) +{ + u64 bytes = (u64)p->pitch * p->height; + + if (!p->address || !p->width || !p->height || + (p->pitch & 3) || (u64)p->width * 4 > p->pitch) + return -EINVAL; + if (bytes > (u32)~0U || bytes > p->backing_bytes || + p->address > (u64)~0ULL - bytes) + return -ERANGE; + return 0; +} + +/* Full-frame linear opaque BGRA. All unused layers stay explicitly null. */ +static inline int dcpext26_primary_encode(void *buffer, size_t capacity, + u32 id, unsigned int layer, + const struct dcpext26_primary *p) +{ + u8 *wire = buffer, *surface; + u32 bytes; + unsigned int i; + int ret; + + if (!buffer || capacity != DCPEXT26_SUBMIT_BYTES || !id || layer >= 4) + return -EINVAL; + if (p) { + ret = dcpext26_primary_check(p); + if (ret) + return ret; + } + memset(wire, 0, capacity); + put_unaligned_le32(id, wire + 0x98); + put_unaligned_le32(0x80000007, wire + 0x14c); + put_unaligned_le32(0x80000007, wire + 0x150); + put_unaligned_le32(0xff000000, wire + 0x154); + /* Ten nullable surfaces, followed by the two output-pointer flags. */ + memset(wire + 0x1bcb, 1, 10); + wire[0x1bd6] = 1; + wire[0x1bd7] = 1; + if (!p) + return 0; + + surface = wire + 0x588 + layer * DCPEXT26_SURFACE_BYTES; + bytes = p->pitch * p->height; + { + const struct { unsigned int offset; u32 value; } words[] = { + {0x21, p->width}, {0x59, p->width}, + {0x25, p->height}, {0x5d, p->height}, + {0x15, p->pitch}, {0x69, p->pitch}, + {0x29, bytes}, {0x6d, bytes}, + {3, 1}, {7, 1}, {0xb, 0x42475241}, {0x35, 1}, + }; + const struct { unsigned int offset; u8 value; } octets[] = { + {2, 1}, {0x13, 13}, {0x14, 12}, {0x19, 1}, + {0x1b, 1}, {0x1c, 1}, {0x71, 4}, {0x73, 1}, {0x74, 1}, + }; + + for (i = 0; i < sizeof(words) / sizeof(words[0]); i++) + put_unaligned_le32(words[i].value, surface + words[i].offset); + for (i = 0; i < sizeof(octets) / sizeof(octets[0]); i++) + surface[octets[i].offset] = octets[i].value; + put_unaligned_le64(1, surface + 0x149); + put_unaligned_le64(1, surface + 0x51); + } + put_unaligned_le64(p->address, wire + 0xe38 + layer * 8); + put_unaligned_le32(1, wire + 0x9c + layer * 4); + for (i = 0; i < 2; i++) { + u8 *rect = wire + (i ? 0x10c : 0xac) + layer * 16; + + put_unaligned_le32(p->width, rect + 8); + put_unaligned_le32(p->height, rect + 12); + } + wire[0x1bcb + layer] = 0; + return 0; +} + +static inline int dcpext26_start_reply(const void *reply, size_t length, u32 *id) +{ + const u8 *wire = reply; + u32 value; + + if (!reply || !id || length != 8) + return -EINVAL; + value = get_unaligned_le32(wire); + if (!value || get_unaligned_le32(wire + 4)) + return -EPROTO; + *id = value; + return 0; +} + +static inline int dcpext26_submit_reply(const void *reply, size_t length) +{ + const u8 *wire = reply; + + if (!reply || length != 12) + return -EINVAL; + return get_unaligned_le32(wire + 5) ? -EPROTO : 0; +} + +#endif From 2d83989795dc8b91bbb5b47f1d99bb77c988f29f Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:34:31 -0500 Subject: [PATCH 12/15] drm/apple: support retained J613 native26 external displays Separate carrier startup, EPIC service identity and controller OPEN from display-port admission. Validate attachment generations, EDID and primary modesets; preserve same-mode readiness across DPMS and repeated commits. Retain the firmware session through s2idle and restart discovery after DRM resume. Release pending flips on a failed modeset without dereferencing an absent backlink. Keep native26 explicitly selected and leave classic peers on their existing protocol. Fold callback, declaration, reply padding, admission, transactional supplier and atomic-primary fixes from the attended C13h series into their owning paths. Omit diagnostic catalogue and lifecycle tracing; keep remaining informational native messages at debug level. Signed-off-by: Satya Benson --- drivers/gpu/drm/apple/afk.c | 106 +++- drivers/gpu/drm/apple/afk.h | 4 + drivers/gpu/drm/apple/apple_drv.c | 11 +- drivers/gpu/drm/apple/dcp-fabric.c | 18 +- drivers/gpu/drm/apple/dcp-fabric.h | 1 + drivers/gpu/drm/apple/dcp-internal.h | 46 +- drivers/gpu/drm/apple/dcp.c | 411 ++++++++++++-- drivers/gpu/drm/apple/dcp.h | 3 + drivers/gpu/drm/apple/dcpext26-lifecycle.h | 42 ++ drivers/gpu/drm/apple/dptxep.c | 208 +++++++- drivers/gpu/drm/apple/dptxep.h | 2 + drivers/gpu/drm/apple/epic/dpavservep.c | 15 +- drivers/gpu/drm/apple/iomfb.c | 163 +++++- drivers/gpu/drm/apple/iomfb.h | 5 + drivers/gpu/drm/apple/iomfb_h17p.c | 591 ++++++++++++++++++++- drivers/gpu/drm/apple/iomfb_internal.h | 5 + drivers/gpu/drm/apple/iomfb_template.c | 171 +++++- drivers/gpu/drm/apple/iomfb_template.h | 7 +- drivers/gpu/drm/apple/plane.c | 30 +- drivers/gpu/drm/apple/plane.h | 2 +- drivers/gpu/drm/apple/systemep.c | 6 +- 21 files changed, 1731 insertions(+), 116 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcpext26-lifecycle.h diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index de9c3846b15b8e..8c07e9f4fafa1e 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -65,7 +65,8 @@ void afk_service_retry(struct apple_dcp_afkep *ep) { unsigned long flags; - if (!ep || !ep->wire_v2 || ep != ep->dcp->dcpavservep) + if (!ep || !ep->wire_v2 || + (!dcp_is_ext26(ep->dcp) && ep != ep->dcp->dcpavservep)) return; spin_lock_irqsave(&ep->lock, flags); if (!ep->stopping) @@ -103,6 +104,7 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, afkep->ops = ops; afkep->dcp = dcp; afkep->endpoint = endpoint; + afkep->wire_v2 = dcp_is_ext26(dcp); afkep->wq = alloc_ordered_workqueue("apple-dcp-afkep%02x", WQ_MEM_RECLAIM, endpoint); if (!afkep->wq) { @@ -424,8 +426,12 @@ static void afk_retry_service_init(struct apple_dcp_afkep *ep) if (!service->pending_open) continue; - ret = ops->revalidate(service, service->pending_name, - service->pending_class, service->pending_unit); + if (dcp_is_ext26(ep->dcp) && + (!READ_ONCE(ep->dcp->ext26_services_open) || + (ep == ep->dcp->dptxep && !dptx_native_controllers_ready(ep->dcp)))) + continue; + ret = ops->revalidate ? ops->revalidate(service, service->pending_name, + service->pending_class, service->pending_unit) : 0; if (ret) { afk_discard_pending_open(service); continue; @@ -443,8 +449,9 @@ static void afk_retry_service_init(struct apple_dcp_afkep *ep) } service->pending_open = false; afk_service_publish(service); - ops->init(service, service->pending_name, service->pending_class, - service->pending_unit); + if (ops->init) + ops->init(service, service->pending_name, service->pending_class, + service->pending_unit); if (service->enabled) afk_populate_service_debugfs(service); kfree(service->pending_name); @@ -477,8 +484,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, const char *epic_name = NULL, *epic_class = NULL; const struct apple_epic_service_ops *ops; struct dcp_parse_ctx ctx; - u8 *props = payload + sizeof(name); - size_t props_size = payload_size - sizeof(name); + u8 *props; + size_t props_size; previous = afk_epic_find_service(ep, channel); /* @@ -503,7 +510,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, if (!pending->pending_open || pending->channel != channel) continue; - if (!pending->ops->revalidate(pending, pending->pending_name, + if (!pending->ops->revalidate || + !pending->ops->revalidate(pending, pending->pending_name, pending->pending_class, pending->pending_unit)) return; afk_discard_pending_open(pending); @@ -516,6 +524,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, } strscpy(name, payload, sizeof(name)); + props = payload + sizeof(name); + props_size = payload_size - sizeof(name); /* * in DCP firmware 13.2 DCP reports interface-name as name which starts @@ -532,8 +542,18 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, return; } ret = parse_epic_service_init(&ctx, &epic_name, &epic_class, &epic_unit, - ep->dcp->fw_compat == DCP_FIRMWARE_H17P); + ep->dcp->fw_compat == DCP_FIRMWARE_H17P && + !dcp_is_ext26(ep->dcp)); if (ret) { + /* These native infrastructure reports have no EPIC identity. */ + if (dcp_is_ext26(ep->dcp) && ret == -ENOENT && + (ep->endpoint == SYSTEM_ENDPOINT || + ep->endpoint == DISP0_ENDPOINT)) { + dev_dbg(ep->dcp->dev, + "native26 AFK ep=%#x interface=%u report=%s has no EPIC identity; ignored\n", + ep->endpoint, channel, name); + return; + } dev_err(ep->dcp->dev, "AFK[ep:%02x]: failed to extract init props: %d\n", ep->endpoint, ret); @@ -548,6 +568,10 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, ops = afk_match_service(ep, epic_name); else ops = afk_match_service(ep, service_name); + if (dcp_is_ext26(ep->dcp)) + dev_dbg(ep->dcp->dev, + "native26 AFK identity ep=%#x interface=%u name=%s class=%s unit=%lld\n", + ep->endpoint, channel, epic_name ?: name, epic_class ?: "", epic_unit); /* T6030 14.7: the DPAV controller endpoint binds whatever it announces. */ if (!ops && ep->dcp->fw_compat == DCP_FIRMWARE_V_14_7 && ep->ops && @@ -558,6 +582,9 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, ops = ep->ops; } + if (!ops && dcp_is_ext26(ep->dcp) && ep->ops && + !strcmp(ep->ops->name, "native26-observer")) + ops = ep->ops; if (!ops) { dev_err(ep->dcp->dev, "AFK[ep:%02x]: unable to match service %s on channel %d\n", @@ -588,13 +615,19 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, "AFK optional %s (%s) is unsupported on channel %u\n", ops->name, service_name, channel); else - dev_err(ep->dcp->dev, "AFK service %s admission failed: %d\n", - service_name, ret); + dev_err(ep->dcp->dev, + "AFK service %s admission failed: %d (ep=%#x interface=%u name=%s unit=%lld)\n", + service_name, ret, ep->endpoint, channel, epic_name ?: name, epic_unit); afk_service_discard(service); goto free; } } - if (previous && retired && ops->revalidate) { + if ((previous && retired && ops->revalidate) || + (dcp_is_ext26(ep->dcp) && + (!READ_ONCE(ep->dcp->ext26_services_open) || + (ep == ep->dcp->dptxep && !dptx_native_controllers_ready(ep->dcp))))) { + /* No reader may use a native interface before its deferred OPEN. */ + WRITE_ONCE(service->enabled, false); /* Preserve the admitted identity and its original provider ticket. */ service->pending_open = true; service->pending_name = epic_name; @@ -1828,6 +1861,22 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, goto err_put_service; } + if (dcp_is_ext26(ep->dcp)) { + size_t charge = ALIGN(payload_len, SZ_16K) + ALIGN(output_len, SZ_16K); + + spin_lock_irqsave(&ep->lock, flags); + ret = READ_ONCE(ep->dcp->crashed) ? -EIO : + (ep->ext26_command_count >= 255 || + charge > SZ_16M - ep->ext26_command_bytes) ? -ENOSPC : 0; + if (!ret) { + ep->ext26_command_bytes += charge; + ep->ext26_command_count++; + } + spin_unlock_irqrestore(&ep->lock, flags); + if (ret) + goto err_put_service; + } + rxbuf = dma_alloc_coherent(ep->dcp->dev, output_len, &rxbuf_dma, GFP_KERNEL); if (!rxbuf) { @@ -1921,7 +1970,9 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, */ if (!service->cmds[idx].done) { service->cmds[idx].completion = NULL; - service->cmds[idx].free_on_ack = true; + service->cmds[idx].free_on_ack = !dcp_is_ext26(ep->dcp); + if (dcp_is_ext26(ep->dcp)) + WRITE_ONCE(ep->dcp->crashed, true); spin_unlock_irqrestore(&service->lock, flags); dev_warn(ep->dcp->dev, "AFK[ep:%02x]: %s (chan:%u) command type 0x%x tag 0x%04x timed out after %u ms\n", @@ -1985,9 +2036,11 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, spin_unlock_irqrestore(&service->lock, flags); err_free_txbuf: kfree(inline_command); - dma_free_coherent(ep->dcp->dev, payload_len, txbuf, txbuf_dma); + if (!dcp_is_ext26(ep->dcp)) + dma_free_coherent(ep->dcp->dev, payload_len, txbuf, txbuf_dma); err_free_rxbuf: - dma_free_coherent(ep->dcp->dev, output_len, rxbuf, rxbuf_dma); + if (!dcp_is_ext26(ep->dcp)) + dma_free_coherent(ep->dcp->dev, output_len, rxbuf, rxbuf_dma); err_put_service: afk_service_put(service); return ret; @@ -2346,3 +2399,26 @@ static void afk_remove_service_debugfs(struct apple_epic_service *srv) #if IS_ENABLED(CONFIG_DRM_APPLE_PARSER_KUNIT_TEST) #include "afk-parser-test.c" #endif + +/* Called after RX and the ordered endpoint worker have been joined. */ +bool afk_host_idle(struct apple_dcp_afkep *ep) +{ + unsigned long flags; + unsigned int i; + bool idle = true; + + spin_lock_irqsave(&ep->lock, flags); + if (ep->rxbfr.ready) + idle = READ_ONCE(*ep->rxbfr.rptr) == READ_ONCE(*ep->rxbfr.wptr); + if (ep->txbfr.ready) + idle &= READ_ONCE(*ep->txbfr.rptr) == READ_ONCE(*ep->txbfr.wptr); + spin_unlock_irqrestore(&ep->lock, flags); + for (i = 0; i < AFK_MAX_CHANNEL && idle; i++) { + struct apple_epic_service *service = &ep->services[i]; + + spin_lock_irqsave(&service->lock, flags); + idle = bitmap_empty(service->cmd_map, MAX_PENDING_CMDS); + spin_unlock_irqrestore(&service->lock, flags); + } + return idle; +} diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 2b6609abe361a5..adc290caaae871 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -449,6 +449,8 @@ struct apple_dcp_afkep { void *bfr; u16 bfr_tag; size_t bfr_size; + size_t ext26_command_bytes; + unsigned int ext26_command_count; dma_addr_t bfr_dma; struct afk_ringbuffer txbfr; @@ -474,6 +476,8 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, void *buffer, size_t size, bool to_peer); int afk_start(struct apple_dcp_afkep *ep); +/* A sampled host boundary, never a remote-producer or DMA fence. */ +bool afk_host_idle(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); int afk_receive_message(struct apple_dcp_afkep *ep, u64 message); diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 2324a8acd4c72a..86c9380a473bc7 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -290,8 +290,13 @@ static void apple_crtc_atomic_enable(struct drm_crtc *crtc, /* A cable generation can change while the CRTC stays active. */ dcp_external_recover(to_apple_crtc(crtc)->dcp); } - if (crtc_state->active) - dcp_crtc_atomic_modeset(crtc, commit); + if (crtc_state->active) { + ret = dcp_crtc_atomic_modeset(crtc, commit); + if (ret && dcp_is_ext26(dcp)) { + dev_err(dcp->dev, "native26 modeset failed (%d); releasing pending flip\n", ret); + goto failed; + } + } dcp_typec_follow_done(locked); return; @@ -683,7 +688,7 @@ static int apple_probe_per_dcp(struct device *dev, planes[zpos] = apple_plane_init(drm, 1U << num, surf, supports_l10r, supports_xrgb2101010, - plane_type, interchange); + plane_type, interchange, dcp_is_ext26(dcp_data)); if (IS_ERR(planes[zpos])) return PTR_ERR(planes[zpos]); diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29321f8032a490..4b9a080c164542 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -426,7 +426,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) int ret = 0; guard(mutex)(&dcp->external_lock); - if (dcp_is_external_h17p(dcp) && + if (dcp_is_retained_external(dcp) && (dcp->external_error || (dcp->external_power_attempted && !dcp->external_stopped))) return dcp->external_error ?: -EAGAIN; @@ -478,7 +478,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) if (sel && sel != -EOPNOTSUPP) dev_warn(dcp->dev, "DP tunnel source reset failed: %d\n", sel); } - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) dpavservep_detach(dcp); route->selected = false; ended_attachment = dcp_modes_end_typec(dcp, route); @@ -1165,7 +1165,7 @@ static bool dcp_rebalance_deactivate(void *data, void *entry) (dcp->typec_connector && dcp->typec_connector->connected)) { ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); /* H17P external pipes keep their own stop recovery, as before. */ - if (ret && !dcp_is_external_h17p(dcp)) + if (ret && !dcp_is_retained_external(dcp)) return true; } port->hpd = false; @@ -1297,7 +1297,7 @@ static int dcp_typec_route_prepare(struct typec_mux_dev *mux, port->dp_wanted = false; port->dp_hpd = false; /* HPD absence alone is not proof of a completed firmware shutdown. */ - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { scoped_guard(mutex, &dcp->external_lock) { if (dcp->external_error) return dcp->external_error; @@ -1435,7 +1435,7 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, (dcp->typec_connector && dcp->typec_connector->connected)) { ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); /* H17P external pipes keep their own stop recovery, as before. */ - if (ret && !dcp_is_external_h17p(dcp)) + if (ret && !dcp_is_retained_external(dcp)) return ret; } } @@ -1763,7 +1763,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) struct mux_control *xbar = NULL; int ret; - if (!of_machine_is_compatible("apple,t6030")) + if (!of_machine_is_compatible("apple,t6030") && !dcp_is_ext26(dcp)) return 0; guard(mutex)(&dcp->tb_lock); route = dcp->active_typec_route; @@ -1774,7 +1774,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) /* A deselected output went idle, and its clocks with it. */ if (dcp->direct_xbar_up && dcp->direct_xbar_up != xbar) dcp->direct_xbar_up = NULL; - if (!xbar || !!dcp->direct_xbar_up == up) + if (!xbar || (!!dcp->direct_xbar_up == up && (up || !dcp_is_ext26(dcp)))) return 0; ret = dcp_dpxbar_link(xbar, up); if (ret) { @@ -3368,7 +3368,7 @@ static void dcp_typec_route_unregister(void *data) if (port->hpd || dcp->typec_cable_connected) dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); port->hpd = false; - if (dcp_typec_route_deactivate(route) && dcp_is_external_h17p(dcp)) + if (dcp_typec_route_deactivate(route) && dcp_is_retained_external(dcp)) return; port->owner = NULL; } @@ -3379,7 +3379,7 @@ static void dcp_typec_route_unregister(void *data) dcp_tb_binding_retire(dcp, route); if (port->hpd || dcp->typec_cable_connected) dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); - if (dcp_typec_route_deactivate(route) && dcp_is_external_h17p(dcp)) + if (dcp_typec_route_deactivate(route) && dcp_is_retained_external(dcp)) return; port->secondary_owner = NULL; } diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 1176ef56874f35..5bd1dd8d0b41cd 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -118,5 +118,6 @@ void dcp_fabric_shutdown_dptx(struct apple_dcp *dcp); int dcp_dptx_connect(struct apple_dcp *dcp, u32 port); int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port); bool dcp_is_external_h17p(struct apple_dcp *dcp); +bool dcp_is_retained_external(struct apple_dcp *dcp); #endif /* __APPLE_DCP_FABRIC_H__ */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 3eac22a57aa069..b2349b871c7f1b 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -39,6 +39,7 @@ #define DCP_MAX_PLANES 4 +struct ext26_control; struct apple_dcp_afkep; struct dcp_method_entry; @@ -183,6 +184,7 @@ enum dcp_iomfb_method_profile { DCP_IOMFB_METHODS_DEFAULT, DCP_IOMFB_METHODS_H17G, DCP_IOMFB_METHODS_H17P_EXTERNAL, + DCP_IOMFB_METHODS_J613_EXT26, }; struct apple_dcp_hw_data { @@ -322,6 +324,41 @@ struct apple_dcp { /* DCP has crashed */ bool crashed; + /* Native26 control/property epochs, serialized by the IOMFB owner. */ + struct completion ext26_link_ready; + bool ext26_services_open; + bool ext26_client_open; + struct apple_dcp_afkep *ext26_aux[3]; + u32 ext26_stride; + u64 ext26_edid_generation; + bool ext26_edid_tried; + u64 ext26_rx_ns; + u64 ext26_power_ns; + u64 ext26_generation; + u64 ext26_buffer_bytes; + bool ext26_blocked; + bool ext26_pm_work_off; + bool ext26_stopping; + bool ext26_blanked; + bool ext26_pm_preparing; + bool ext26_control_pending; + bool ext26_early_complete; + u32 ext26_early_id; + u64 ext26_early_ns; + u32 ext26_control_id; + struct ext26_control *ext26_control; + struct work_struct ext26_blank_work; + struct list_head ext26_retained; + u64 ext26_retained_bytes; + unsigned int ext26_epochs; + struct drm_display_mode ext26_mode; + DECLARE_BITMAP(ext26_released, DCP_MAX_MAPPINGS); + bool ext26_power_sent; + bool ext26_normal; + struct completion ext26_ready; + iomfb_cb_handler *ext26_handlers; + u8 ext26_present[0x1bd8]; + DECLARE_BITMAP(iomfb_surfaces, DCP_MAX_PLANES); @@ -734,6 +771,11 @@ static inline bool dcp_fw_is_14_7(const struct apple_dcp *dcp) dcp->hw.fw_14_7_endpoints); } +static inline bool dcp_is_ext26(const struct apple_dcp *dcp) +{ + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_J613_EXT26; +} + /* * The T6030 external processor (the J514S/J516S built-in HDMI port and * USB-C displays) driven natively through Aurora's DCPLink session: DCP @@ -766,8 +808,8 @@ void dcp_external_audio_start(struct apple_dcp *dcp); static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) { /* Not the generic M1/M2 DCPs this tree also runs on the template. */ - return dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && - dcp->hw.fw_14_7_endpoints; + return (dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && + dcp->hw.fw_14_7_endpoints); } #define DCP_AUDIO_MAX_CHANS 15 diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index a8571b5446f5c4..06a5908828898a 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2,6 +2,7 @@ /* Copyright 2021 Alyssa Rosenzweig */ #include +#include #include #include #include @@ -51,9 +52,11 @@ #include "dcp-hdmi.h" #include "dcpext_scanout.h" #include "dcp-internal.h" +#include "j613-25g83.h" #include "dcp-external-bw.h" #include "dptx-service.h" #include "iomfb.h" +#include "iomfb_internal.h" #include "ibootep.h" #include "parser.h" #include "trace.h" @@ -215,7 +218,49 @@ bool dcp_is_external_h17p(struct apple_dcp *dcp) return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; } +bool dcp_is_retained_external(struct apple_dcp *dcp) +{ + return dcp_is_external_h17p(dcp) || dcp_is_ext26(dcp); +} + +static void dcp_ext26_blank_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(work, struct apple_dcp, ext26_blank_work); + int ret; + + guard(mutex)(&dcp->external_lock); + if (dcp->ext26_pm_preparing || dcp->external_stopped || + !READ_ONCE(dcp->pipe_enabled_h17p)) + return; + iomfb_pause_presentation(dcp, true); + ret = iomfb_ext26_blank(dcp); + if (ret) { + WRITE_ONCE(dcp->ext26_blocked, true); + dcp->external_error = ret; + dev_err(dcp->dev, "native26 null control failed: %d; uncertain backing retained\n", ret); + } else { + WRITE_ONCE(dcp->ext26_blanked, true); + dev_dbg(dcp->dev, "native26 DPMS blank complete; link, mode and backing retained\n"); + } +} + static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation); +int dcp_ext26_layout_stop(struct apple_dcp *dcp) +{ + int ret; + + /* Atomic modeset owns external_lock; poweron joined the blank worker. */ + lockdep_assert_held(&dcp->external_lock); + ret = dcp_external_stop_locked(dcp, dcp->external_generation); + if (!ret) { + WRITE_ONCE(dcp->ext26_blanked, false); + dcp->external_generation++; + dcp_queue_typec_reconnect(dcp, 0); + return -EAGAIN; /* A fresh owner/catalogue/backing is required. */ + } + return ret; +} + static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical); /* copied and simplified from drm_vblank.c */ @@ -634,6 +679,14 @@ static void dcp_delayed_vblank(struct work_struct *work) static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) { + if (dcp_is_ext26(dcp)) { + if (endpoint == 0x27) + return dcp->ext26_aux[0]; + if (endpoint == 0x29) + return dcp->ext26_aux[1]; + if (endpoint == 0x2c) + return dcp->ext26_aux[2]; + } /* DPAV 0x2a on H17P overlaps the legacy DPTX endpoint number. */ if (dcp->dcpavservep && endpoint == dcp->dcpavservep->endpoint) return dcp->dcpavservep; @@ -710,7 +763,7 @@ void dcp_swap_watchdog_complete(struct apple_dcp *dcp) void dcp_external_link_fault(struct apple_dcp *dcp) { dcp_180hz_invalidate(dcp); - if (dcp_is_external_h17p(dcp) && dcp_mode_invalidate(&dcp->mode_state)) + if (dcp_is_retained_external(dcp) && dcp_mode_invalidate(&dcp->mode_state)) dcp_handle_hotplug_actions(dcp, DCP_HOTPLUG_NOTIFY); } @@ -755,6 +808,15 @@ static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message) afk_receive_message(ep, message); } +static void dcp_recv_msg_timed(void *cookie, u8 endpoint, u64 message, u64 received_ns) +{ + struct apple_dcp *dcp = cookie; + + if (dcp_is_ext26(dcp) && endpoint == IOMFB_ENDPOINT) + dcp->ext26_rx_ns = received_ns; + dcp_recv_msg(cookie, endpoint, message); +} + static void dcp_rtk_crashed(void *cookie, const void *crashlog, size_t crashlog_size) { struct apple_dcp *dcp = cookie; @@ -878,6 +940,7 @@ static void dcp_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) static struct apple_rtkit_ops rtkit_ops = { .crashed = dcp_rtk_crashed, .recv_message = dcp_recv_msg, + .recv_message_timed = dcp_recv_msg_timed, .shmem_setup = dcp_rtk_shmem_setup, .shmem_destroy = dcp_rtk_shmem_destroy, }; @@ -886,6 +949,7 @@ static const struct apple_rtkit_ops external_h17p_rtkit_ops = { .flags = APPLE_RTKIT_COPROC_PLACES_BUFFERS, .crashed = dcp_rtk_crashed, .recv_message = dcp_recv_msg, + .recv_message_timed = dcp_recv_msg_timed, .shmem_setup = dcp_rtk_shmem_setup, .shmem_destroy = dcp_rtk_shmem_destroy, }; @@ -895,7 +959,8 @@ void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message) bool native_14_7 = dcp->fw_compat == DCP_FIRMWARE_V_14_7; int ret; - if (READ_ONCE(dcp->quiescing)) + if (READ_ONCE(dcp->quiescing) || + (dcp_is_ext26(dcp) && READ_ONCE(dcp->ext26_blocked))) return; trace_dcp_send_msg(dcp, endpoint, message); /* @@ -904,7 +969,11 @@ void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message) * DisplayPort handshake then waits for a reply that was never sent. */ ret = apple_rtkit_send_message(dcp->rtk, endpoint, message, NULL, - native_14_7 ? in_atomic() : true); + (native_14_7 || dcp_is_ext26(dcp)) ? in_atomic() : true); + if (ret && dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->crashed, true); + dev_err(dcp->dev, "native26 mailbox send failed ep=%#x: %d\n", endpoint, ret); + } if (ret && native_14_7) dev_warn_ratelimited(dcp->dev, "DCP send ep %02x failed: %d\n", endpoint, ret); @@ -1030,7 +1099,7 @@ static bool dcp_connect_session_valid(struct apple_dcp *dcp, /* H17P (J700, T8140, T8142) connects and reconnects as before, without one. */ static bool dcp_typec_sessionless(struct apple_dcp *dcp) { - return dcp->fw_compat == DCP_FIRMWARE_H17P || dcp_is_external_h17p(dcp); + return dcp->fw_compat == DCP_FIRMWARE_H17P || dcp_is_retained_external(dcp); } /* Capture the session at enqueue, not when a stale worker eventually runs. */ @@ -1122,6 +1191,11 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable dcp->dptxport[port].atcphy = dcp->phy; /* a tiled display's second half comes in on the port's dpin1 */ dfp_port = port && READ_ONCE(dcp->split.active) ? 2 : dcp->dptx_dfp_port; + if (dcp_is_ext26(dcp)) { + ret = dcp_direct_crossbar_link(dcp, false); + if (ret) + goto out_unlock; + } if (!dcp_ext_dptx_607(dcp)) { ret = dptxport_validate_connection(dcp->dptxport[port].service, dfp_port, @@ -1250,7 +1324,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable /* Do not timestamp a new attachment after an older link wait. */ if (!port && dcp_is_typec_output(dcp) && !dcp_is_usb4_output(dcp) && dcp->fw_compat != DCP_FIRMWARE_H17P && !dcp->external_native && - !dcp_is_external_h17p(dcp)) { + !dcp_is_retained_external(dcp)) { guard(mutex)(&dcp->hpd_mutex); if (dcp->typec_generation == generation && dcp->typec_cable_connected) { scoped_guard(spinlock_irqsave, &dcp->relink_lock) @@ -1262,7 +1336,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable out_withdraw: mutex_lock(&dcp->hpd_mutex); - if (!dcp_is_external_h17p(dcp)) { + if (!dcp_is_retained_external(dcp)) { dcp->dptxport[port].connected = false; goto out_release; } @@ -1332,7 +1406,9 @@ static int dcp_external_link_locked(struct apple_dcp *dcp) int ret; lockdep_assert_held(&dcp->external_lock); - if (!dcp->external_prepared) + if (!dcp->external_prepared || + (dcp_is_ext26(dcp) && (!READ_ONCE(dcp->ext26_services_open) || + !dptx_native_controllers_ready(dcp)))) return -EAGAIN; if (dcp->external_error) return dcp->external_error; @@ -1350,8 +1426,18 @@ static int dcp_external_start_locked(struct apple_dcp *dcp) lockdep_assert_held(&dcp->external_lock); ret = dcp_external_link_locked(dcp); - if (ret || dcp->external_running) + if (ret) return ret; + /* Gentle DPMS kept the link running but paused presentation. */ + if (dcp_is_ext26(dcp)) { + dcp->external_running = true; + dcp->external_stopped = false; + WRITE_ONCE(dcp->typec_crtc_off, false); + iomfb_pause_presentation(dcp, false); + return 0; + } + if (dcp->external_running) + return 0; /* A deferred stop sent nothing: the pipe is still powered. */ if (dcp->external_stop_deferred) { dcp->external_stop_deferred = false; @@ -1621,7 +1707,7 @@ int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) struct dcp_fabric_session session = {}; int ret; - if (!dcp_is_external_h17p(dcp)) { + if (!dcp_is_retained_external(dcp)) { if (dcp_typec_sessionless(dcp)) { ret = dcp_dptx_connect_raw(dcp, port, NULL, NULL, false, false); } else { @@ -1655,6 +1741,10 @@ static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation) if (dcp->external_stopped && !READ_ONCE(dcp->dptxport[0].connected)) return 0; iomfb_pause_presentation(dcp, true); + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_stopping, true); + dpavservep_detach(dcp); + } dcp->external_running = false; /* Route setup can call this before IOMFB or its shared memory exists. */ if (dcp->external_stopped || !dcp->external_power_attempted) @@ -1704,7 +1794,33 @@ static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation) dcp->dptxport[0].connected = false; } } + if (dcp_is_ext26(dcp)) { + struct apple_dcp_afkep *endpoints[] = {dcp->systemep, dcp->ibootep, + dcp->dptx_controller_ep, dcp->dptxep, dcp->dcpavservep, + dcp->ext26_aux[0], dcp->ext26_aux[1], dcp->ext26_aux[2]}; + unsigned int i; + + step = "draining the retained epoch"; + apple_rtkit_flush_rx(dcp->rtk); + for (i = 0; i < ARRAY_SIZE(endpoints); i++) + if (!IS_ERR_OR_NULL(endpoints[i])) + flush_workqueue(endpoints[i]->wq); + apple_rtkit_flush_rx(dcp->rtk); + ret = iomfb_queue_settle(dcp, msecs_to_jiffies(5000)); + for (i = 0; !ret && i < ARRAY_SIZE(endpoints); i++) + if (!IS_ERR_OR_NULL(endpoints[i]) && !afk_host_idle(endpoints[i])) + ret = -EBUSY; + if (!ret) + ret = iomfb_ext26_retain_epoch(dcp); + if (ret) + goto failed; + dcp_modes_begin_attachment(dcp); + WRITE_ONCE(dcp->pipe_enabled_h17p, false); + WRITE_ONCE(dcp->ext26_normal, false); + } dcp->external_stopped = true; + if (dcp_is_ext26(dcp)) + WRITE_ONCE(dcp->ext26_stopping, false); WRITE_ONCE(dcp->typec_crtc_off, true); if (dcp->avep) av_service_disconnect(dcp); @@ -1722,10 +1838,14 @@ static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical) cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); + if (dcp_is_ext26(dcp)) + flush_work(&dcp->ext26_blank_work); guard(mutex)(&dcp->external_lock); /* Invalidate physical EDID before waiting for firmware power/link teardown. */ if (physical) { WRITE_ONCE(dcp->external_physically_absent, true); + if (dcp_is_ext26(dcp)) + dcp_modes_begin_attachment(dcp); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; @@ -1778,10 +1898,24 @@ static void dcp_external_stop_work(struct work_struct *work) bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) { - if (!dcp_is_external_h17p(dcp)) + if (!dcp_is_retained_external(dcp)) return false; /* No lifecycle or HPD mutex may be acquired from the RX callback. */ lockdep_assert_held(&dcp->iomfb.lock); + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->ext26_pm_preparing) || READ_ONCE(dcp->ext26_stopping) || + READ_ONCE(dcp->quiescing) || + READ_ONCE(dcp->external_physically_absent)) + return true; + if (connected) + return false; + if (READ_ONCE(dcp->external_stopped)) + return true; + dcp->external_stop_generation = dcp->external_rx_generation; + WRITE_ONCE(dcp->ext26_normal, false); + schedule_work(&dcp->external_stop_work); + return true; + } if (READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->external_physically_absent)) return true; if (dcp->iomfb.presentation_paused) { @@ -1970,7 +2104,7 @@ bool dcp_hold_resume_event(struct platform_device *pdev) struct apple_dcp *dcp = platform_get_drvdata(pdev); struct apple_connector *con = READ_ONCE(dcp->connector); - if (!dcp_is_external_h17p(dcp) || !con || READ_ONCE(con->dcp) != pdev) + if (!dcp_is_retained_external(dcp) || !con || READ_ONCE(con->dcp) != pdev) return false; guard(mutex)(&dcp->hpd_mutex); @@ -2030,7 +2164,7 @@ static bool dcp_typec_relink_eligible(struct apple_dcp *dcp) /* H17P and native external pipes recover through their own lifecycle */ if (dcp->fw_compat == DCP_FIRMWARE_H17P || dcp->external_native || !dcp_is_typec_output(dcp) || dcp_is_usb4_output(dcp) || - dcp_is_external_h17p(dcp) || !dcp->typec_connector) + dcp_is_retained_external(dcp) || !dcp->typec_connector) return false; /* * Only a sink that is attached, shown and still connected for DRM: @@ -2187,7 +2321,7 @@ int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) { int ret; - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) return dcp_external_disconnect(dcp, false); /* Release the caller's RemotePort service, not the downstream DFP port. */ dev_info(dcp->dev, "%s(port=%d)\n", __func__, port); @@ -2205,7 +2339,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { bool new_cable; cancel_delayed_work_sync(&dcp->typec_reconnect_wq); @@ -2246,7 +2380,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) return dcp_external_disconnect(dcp, true); if (dcp_is_typec_output(dcp)) { @@ -2294,7 +2428,7 @@ int dcp_dptx_recover_irq(struct apple_dcp *dcp) int ret; mutex_lock(&dcp->hpd_mutex); - if (dcp->external_native || dcp_is_external_h17p(dcp) || + if (dcp->external_native || dcp_is_retained_external(dcp) || !dcp_is_typec_output(dcp) || dcp_is_usb4_output(dcp) || !dcp->typec_connector || READ_ONCE(dcp->typec_connector->connected) || @@ -2420,6 +2554,78 @@ unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev) return dcp->iomfb_surfaces; } +static int ext26_aux_call(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + memset(reply, 0, room); + memcpy(reply, data, min(size, room)); + if (room >= sizeof(u32)) + put_unaligned_le32(0, reply); + return 0; +} + +static const struct apple_epic_service_ops ext26_aux_ops[] = { + { .name = "native26-observer", .call_v2 = ext26_aux_call }, {} +}; + +void dcp_ext26_open_services(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *eps[] = { dcp->systemep, dcp->ibootep, + dcp->dptx_controller_ep, dcp->ext26_aux[0], dcp->dcpavservep, + dcp->ext26_aux[1], dcp->dptxep, dcp->ext26_aux[2] }; + unsigned int i; + + WRITE_ONCE(dcp->ext26_services_open, true); + for (i = 0; i < ARRAY_SIZE(eps); i++) + if (!IS_ERR_OR_NULL(eps[i])) + afk_service_retry(eps[i]); +} + +static int dcp_ext26_start_services(struct apple_dcp *dcp) +{ + static const u8 auxiliary[] = { 0x27, 0x29, 0x2c }; + unsigned int i; + int ret; + + ret = iomfb_start_rtkit(dcp); + if (ret) + return ret; + if (!wait_for_completion_timeout(&dcp->ext26_link_ready, msecs_to_jiffies(5000))) + return dev_err_probe(dcp->dev, -ETIMEDOUT, "native26 DCPLink readiness timed out\n"); + if (READ_ONCE(dcp->crashed)) + return -EIO; + ret = systemep_init(dcp); + if (!ret) + ret = ibootep_init(dcp); + if (!ret) + ret = dptx_controller_init(dcp); + for (i = 0; !ret && i < ARRAY_SIZE(auxiliary); i++) { + struct apple_dcp_afkep *ep; + + if (i == 1) { + ret = dpavservep_init(dcp); + if (ret) + break; + } + if (i == 2) { + ret = dptxep_init(dcp); + if (ret) + break; + } + ep = afk_init(dcp, auxiliary[i], ext26_aux_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + ep->match_epic_name = false; + dcp->ext26_aux[i] = ep; + ret = afk_start(ep); + } + if (ret) + return dev_err_probe(dcp->dev, ret, "native26 AFK startup failed\n"); + dev_dbg(dcp->dev, "native26 AFK ready: 20 23 24 27 28 29 2a 2c; starting A401\n"); + iomfb_ext26_start(dcp); + return 0; +} + int dcp_start(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2450,6 +2656,8 @@ int dcp_start(struct platform_device *pdev) if (IS_ERR_OR_NULL(dcp->rtk)) return -ENODEV; + if (dcp_is_ext26(dcp)) + return dcp_ext26_start_services(dcp); /* Make the H17P allocator available before external service discovery. */ if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { @@ -2470,14 +2678,14 @@ int dcp_start(struct platform_device *pdev) ret); } - if (dcp->phy && dcp->fw_compat >= DCP_FIRMWARE_V_13_5) { + if ((dcp->phy || dcp_is_ext26(dcp)) && dcp->fw_compat >= DCP_FIRMWARE_V_13_5) { ret = ibootep_init(dcp); if (ret) dev_warn(dcp->dev, "Failed to start IBOOT endpoint: %d\n", ret); if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(dcp)) { ret = dptx_controller_init(dcp); if (ret) return dev_err_probe(dcp->dev, ret, @@ -2522,7 +2730,7 @@ int dcp_start(struct platform_device *pdev) } #if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) - if (hdmi_audio) { + if (hdmi_audio && !dcp_is_ext26(dcp)) { ret = avep_init(dcp); if (ret) dev_warn(dcp->dev, "Failed to start AV endpoint: %d", ret); @@ -2747,7 +2955,9 @@ void dcp_poweron(struct platform_device *pdev) unsigned long remaining; int ret; - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_ext26(dcp)) + flush_work(&dcp->ext26_blank_work); + if (dcp_is_retained_external(dcp)) { cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); cancel_work_sync(&dcp->external_stop_work); @@ -2885,7 +3095,7 @@ void dcp_external_recover(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) dcp_poweron(pdev); } @@ -2895,7 +3105,12 @@ void dcp_poweroff(struct platform_device *pdev) bool display_gone = false; int ret; - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_ext26(dcp)) { + if (!READ_ONCE(dcp->ext26_pm_preparing) && !READ_ONCE(dcp->external_stopped)) + queue_work(system_unbound_wq, &dcp->ext26_blank_work); + return; + } + if (dcp_is_retained_external(dcp)) { cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); guard(mutex)(&dcp->external_lock); @@ -3433,6 +3648,27 @@ static int dcp_read_fw_version(struct device *dev, const char *name, return len; } +static bool dcp_ext26_admitted(struct device_node *np) +{ + struct device_node *chosen = of_find_node_by_path("/chosen"); + const char *opt, *uuid; + int length = 0; + u32 version[3], marker; + bool ok; + + opt = chosen ? of_get_property(chosen, "asahi,j613-25g83-dcpext", &length) : NULL; + ok = opt && length == 2 && opt[0] == '1' && !opt[1] && + of_machine_is_compatible("apple,j613") && + !of_property_read_u32(np, "apple,j613-25g83-dcpext-handoff", &marker) && marker == 1 && + !of_property_read_u32(np, "apple,t8122-dcpext-firmware-ready", &marker) && marker == 1 && + of_property_count_u32_elems(np, "apple,firmware-compat") == 3 && + !of_property_read_u32_array(np, "apple,firmware-compat", version, 3) && + !of_property_read_string(np, "apple,firmware-uuid", &uuid) && + j613_25g83_identity(true, 1, version, 3, uuid); + of_node_put(chosen); + return ok; +} + static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) { const struct apple_dcp_hw_data *hw = of_device_get_match_data(dev); @@ -3491,6 +3727,8 @@ static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) * firmware through the classic IOMFB template, as on M1/M2. */ if (of_device_is_compatible(dev->of_node, "apple,t8122-dcpext")) { + if (dcp_ext26_admitted(dev->of_node)) + return DCP_FIRMWARE_H17P; if (!strcmp(compat_str, "14.7.0")) return DCP_FIRMWARE_V_14_7_TEMPLATE; dev_err(dev, "T8122 external display not started: DCP firmware-compat %s is not 14.7.0\n", @@ -3574,7 +3812,7 @@ VISIBLE_IF_KUNIT void dcp_disable_typec_work(struct apple_dcp *dcp, bool release /* Nothing arms the timeout work past its drain below. */ dcp->announce.stopped = true; } - if (release_cable && dcp_is_external_h17p(dcp)) + if (release_cable && dcp_is_retained_external(dcp)) dpavservep_detach(dcp); /* Block new enqueues as well as draining users of the AFK endpoints. */ disable_delayed_work_sync(&dcp->typec_reconnect_wq); @@ -3606,7 +3844,7 @@ VISIBLE_IF_KUNIT void dcp_enable_typec_work(struct apple_dcp *dcp) /* A cable can be routed before the DRM component binds. */ if (READ_ONCE(dcp->typec_cable_connected)) dcp_queue_typec_reconnect(dcp, 0); - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { struct apple_connector *con = READ_ONCE(dcp->connector); guard(mutex)(&dcp->hpd_mutex); @@ -3683,7 +3921,7 @@ static int dcp_ext_wait_pmp(struct apple_dcp *dcp) unsigned long deadline; int ret; - if (!dcp_ext_dptx_607(dcp)) + if (!dcp_ext_dptx_607(dcp) && !dcp_is_ext26(dcp)) return 0; entry = of_parse_phandle(dcp->dev->of_node, "apple,pmp-report", 0); if (!entry) { @@ -3725,6 +3963,37 @@ static bool dcp_panel_is_j313(const struct dcp_panel_id *id, return id && !strcmp(id->name, "J313"); } +/* One initial resident session; reconnect and PM never call this routine. */ +static int dcp_ext26_start_resident(struct apple_dcp *dcp) +{ + struct device *dev = dcp->dev; + u32 control; + int ret; + + ret = dcp_pin_live_session(dcp); + if (ret) + return ret; + control = readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); + dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, &rtkit_ops); + if (IS_ERR(dcp->rtk)) + return dev_err_probe(dev, PTR_ERR(dcp->rtk), "native26 RTKit mailbox initialization failed\n"); + ret = devm_add_action_or_reset(dev, dcp_release_rtkit, dcp); + if (ret) + return ret; + if (!(control & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) { + writel(control | APPLE_DCP_COPROC_CPU_CONTROL_RUN, + dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); + dev_dbg(dev, "native26 resident ASC started once after RTKit mailbox initialization\n"); + } + ret = control & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? + apple_rtkit_wake(dcp->rtk) : apple_rtkit_boot(dcp->rtk); + dev_dbg(dev, "native26 initial RTKit %s result=%d control=%#x status=%#x\n", + control & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? "warm INIT" : "resident boot", + ret, readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL), + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS)); + return ret; +} + /* * The boot loader hands a panel over lit as an available simple-framebuffer * whose "panel" points at it, until the first power-on has cycled it (see @@ -3788,7 +4057,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat return ret; /* A native external processor mapped these at probe, for its start work. */ - if (!dcp->external_native) { + if (!dcp->external_native && !dcp_is_ext26(dcp)) { dcp->coproc_reg = devm_platform_ioremap_resource_byname(to_platform_device(dev), "coproc"); if (IS_ERR(dcp->coproc_reg)) @@ -3804,7 +4073,11 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (dcp->hw.adopt_live_session) { cpu_ctrl = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); - if (!(cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) + if (dcp_is_ext26(dcp)) + dev_dbg(dev, "native26 initial carrier: control=%#x status=%#x; RTKit %s\n", + cpu_ctrl, readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS), + cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? "warm INIT" : "resident first boot"); + if (!(cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN) && !dcp_is_ext26(dcp)) return dev_err_probe(dev, -EOPNOTSUPP, "DCP was not left running by the bootloader\n"); } @@ -3976,6 +4249,13 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (ret) goto err_piodma; + if (dcp_is_ext26(dcp)) { + ret = dcp_ext26_start_resident(dcp); + if (ret) + goto err_piodma; + goto session_ready; + } + if (!dcp->hw.adopt_live_session) { cpu_ctrl = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); @@ -4016,6 +4296,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat ret = dev_err_probe(dev, ret, "Failed to boot RTKit: %d\n", ret); goto err_piodma; } +session_ready: enable_work(&dcp->dimensions_wq); schedule_work(&dcp->dimensions_wq); dcp_enable_typec_work(dcp); @@ -4090,6 +4371,8 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v return; } + if (dcp_is_ext26(dcp)) + cancel_work_sync(&dcp->ext26_blank_work); if (dcp->retain_dma) { /* No firmware stop is acknowledged: detach host users, never DMA. */ scoped_guard(mutex, &dcp->iomfb.lock) @@ -4349,6 +4632,23 @@ 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); + if (of_device_is_compatible(dev->of_node, "apple,t8122-dcpext") && + fw_compat == DCP_FIRMWARE_H17P) { + dcp->hw.iomfb_method_profile = DCP_IOMFB_METHODS_J613_EXT26; + dcp->hw.adopt_live_session = true; + dcp->hw.fw_14_7_endpoints = false; + init_completion(&dcp->ext26_link_ready); + init_completion(&dcp->ext26_ready); + INIT_WORK(&dcp->ext26_blank_work, dcp_ext26_blank_work); + INIT_LIST_HEAD(&dcp->ext26_retained); + dcp->coproc_reg = devm_platform_ioremap_resource_byname(pdev, "coproc"); + if (IS_ERR(dcp->coproc_reg)) + return PTR_ERR(dcp->coproc_reg); + dev_dbg(dev, "native26 probe carrier: control=%#x status=%#x; firmware CPU lease supplied by loader\n", + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL), + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS)); + } + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { if (!dcp->hw.firmware_clock || @@ -4457,6 +4757,11 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) set_bit(surf, dcp->iomfb_surfaces); surf++; } + if (dcp_is_ext26(dcp)) { + bitmap_zero(dcp->iomfb_surfaces, DCP_MAX_PLANES); + set_bit(0, dcp->iomfb_surfaces); + } + /* The J700 loader names surface zero rather than enabling it. */ if (of_machine_is_compatible("apple,j700") && of_device_is_compatible(dev->of_node, "apple,t8140-dcp") && @@ -4618,8 +4923,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) return iomfb_v14_7_external_start(dcp); } - if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) { + if (dcp_is_retained_external(dcp)) { /* Complete optional startup before the DRM master can depend on it. */ ret = dcp_comp_bind(dev, NULL, NULL); if (ret) { @@ -4954,6 +5258,15 @@ static int dcp_platform_prepare(struct device *dev) struct apple_dcp *dcp = dev_get_drvdata(dev); const char *why; + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->crashed) || READ_ONCE(dcp->ext26_blocked)) + return -EIO; + WRITE_ONCE(dcp->ext26_pm_preparing, true); + cancel_work_sync(&dcp->ext26_blank_work); + dcp_disable_typec_work(dcp, false); + dcp->ext26_pm_work_off = true; + return 0; + } if (!dcp->external) return 0; mutex_lock(&dcp->hpd_mutex); @@ -4975,6 +5288,16 @@ static void dcp_platform_complete(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_pm_preparing, false); + if (dcp->ext26_pm_work_off && !READ_ONCE(dcp->ext26_blocked) && + !READ_ONCE(dcp->crashed)) { + dcp_enable_typec_work(dcp); + dcp->ext26_pm_work_off = false; + } + return; + } + if (!dcp->external) return; mutex_lock(&dcp->hpd_mutex); @@ -4986,6 +5309,10 @@ static int dcp_platform_suspend(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + if (dcp_is_ext26(dcp) && pm_suspend_target_state != PM_SUSPEND_TO_IDLE) { + dev_warn(dev, "native26 refuses non-s2idle target %d\n", pm_suspend_target_state); + return -EBUSY; + } if (READ_ONCE(dcp->ext_dead)) return 0; iomfb_v14_7_backlight_suspend(dcp); @@ -5011,16 +5338,23 @@ static int dcp_platform_suspend(struct device *dev) mutex_unlock(&dcp->hpd_mutex); } - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { int ret; - dcp_disable_typec_work(dcp, false); + if (!dcp_is_ext26(dcp)) + dcp_disable_typec_work(dcp, false); ret = dcp_external_disconnect(dcp, false); if (ret) { - dcp_enable_typec_work(dcp); + if (!dcp_is_ext26(dcp)) + dcp_enable_typec_work(dcp); return ret; } + if (dcp_is_ext26(dcp) && + !(readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL) & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) + return -EHOSTDOWN; device_set_wakeup_path(dev); + if (dcp_is_ext26(dcp)) + dev_dbg(dev, "native26 retained s2idle stop; firmware and supplier leases held\n"); return 0; } @@ -5065,6 +5399,19 @@ static int dcp_platform_resume(struct device *dev) if (READ_ONCE(dcp->ext_dead)) return 0; + if (dcp_is_ext26(dcp)) { + if (!(readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL) & APPLE_DCP_COPROC_CPU_CONTROL_RUN) || + READ_ONCE(dcp->crashed)) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->external_error, -EHOSTDOWN); + dev_err(dev, "native26 firmware did not survive s2idle; retained session requires reboot\n"); + return -EHOSTDOWN; + } + WRITE_ONCE(dcp->external_suspended, false); + /* Complete restarts discovery after DRM has dropped the stale CRTC. */ + dev_dbg(dev, "native26 resumed retained firmware; discovery waits for PM complete\n"); + return 0; + } dcp_release_after_sleep(dcp); iomfb_v14_7_backlight_resume(dcp); dcp_enable_typec_work(dcp); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index baff04ee201e72..9789abd0ca010e 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -66,6 +66,8 @@ bool dcp_fw_compat_is_14_7(struct platform_device *pdev); bool dcp_ext_unusable(struct platform_device *pdev); unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev); int dcp_start(struct platform_device *pdev); +void dcp_ext26_open_services(struct apple_dcp *dcp); +int dcp_ext26_layout_stop(struct apple_dcp *dcp); int dcp_wait_ready(struct platform_device *pdev, u64 timeout); void dcp_flush(struct drm_crtc *crtc, struct drm_atomic_commit *commit); void dcp_retain_framebuffer(struct platform_device *pdev, @@ -109,6 +111,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port); bool dcp_hold_resume_event(struct platform_device *pdev); int iomfb_start_rtkit(struct apple_dcp *dcp); +void iomfb_ext26_start(struct apple_dcp *dcp); void iomfb_shutdown(struct apple_dcp *dcp); /* rtkit message handler for IOMFB messages */ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message); diff --git a/drivers/gpu/drm/apple/dcpext26-lifecycle.h b/drivers/gpu/drm/apple/dcpext26-lifecycle.h new file mode 100644 index 00000000000000..4eb85f49e22e98 --- /dev/null +++ b/drivers/gpu/drm/apple/dcpext26-lifecycle.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCPEXT26_LIFECYCLE_H +#define APPLE_DCPEXT26_LIFECYCLE_H + +#include + +/* A protocol control completion grants no permission to retire DMA backing. */ +struct dcpext26_exchange { + u32 id; + bool replied; + bool completed; + bool failed; +}; + +static inline bool dcpext26_exchange_begin(struct dcpext26_exchange *x, u32 id) +{ + if (!id || x->id) + return false; + x->id = id; + return true; +} + +static inline bool dcpext26_exchange_complete(struct dcpext26_exchange *x, u32 id) +{ + if (!x->id || id != x->id) + return false; + x->completed = true; + return true; +} + +static inline bool dcpext26_exchange_finished(const struct dcpext26_exchange *x) +{ + return x->failed || (x->id && x->replied && x->completed); +} +/* Reserve headroom for an in-flight full-frame primary before readmission. */ +static inline bool dcpext26_epoch_fits(unsigned int epochs, u64 retained, u64 incoming) +{ + const u64 limit = (1ULL << 30) - (64ULL << 20); + + return !incoming || (epochs < 8 && retained <= limit && incoming <= limit - retained); +} +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 741f40bc8c9ed5..517e0864f8791a 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -569,7 +569,8 @@ static int dptxport_call_set_link_rate(struct apple_epic_service *service, } } else if (dptx->atcphy) { /* No clocked crossbar output across a PHY rate change. */ - dcp_direct_crossbar_link(service->ep->dcp, false); + if (!dcp_is_ext26(service->ep->dcp)) + dcp_direct_crossbar_link(service->ep->dcp, false); dptx->phy_ops.dp.link_rate = phy_link_rate; dptx->phy_ops.dp.set_rate = 1; ret = phy_configure(dptx->atcphy, &dptx->phy_ops); @@ -916,7 +917,7 @@ static int dptxport_prepare(struct apple_epic_service *service, const char *name * T6030 14.7 also announces the low-power port service, and may leave * its class empty. */ - if (dcp_fw_is_14_7(dcp) && name && + if ((dcp_fw_is_14_7(dcp) || dcp_is_ext26(dcp)) && name && (!strcmp(name, "dcpdptx-port-epic") || !strcmp(name, "dcp-lpdptx-port-epic")) && (!class || !class[0] || !strcmp(class, "AppleDCPDPTXRemotePort"))) @@ -942,8 +943,8 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, if (service->ep->dcp->external_native) schedule_work(&service->ep->dcp->external_ready_work); /* Publication may follow the startup wait or the bounded retry window. */ - if (service->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL && + if ((service->ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(service->ep->dcp)) && READ_ONCE(service->ep->dcp->typec_cable_connected) && !READ_ONCE(service->ep->dcp->quiescing) && !READ_ONCE(service->ep->dcp->external_error)) @@ -951,9 +952,140 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, &service->ep->dcp->typec_reconnect_wq, 0); } +/* Native26 USB-C callbacks use the current Type-C PHY without changing its mode. */ +static int dptxport_native26_call(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + struct dptx_port *port = service->cookie; + struct apple_dcp *dcp = service->ep->dcp; + union phy_configure_opts capacity = {}; + int ret; + + if (group == 1 && (command == 3 || command == 4)) + return dptx_j700_training(service, command == 3, data, size, reply, room); + if (group || (size && !data)) + return -EOPNOTSUPP; + memset(reply, 0, room); + memcpy(reply, data, min(size, room)); + if (room >= 4) + put_unaligned_le32(0, reply); + switch (command) { + case DPTX_APCALL_ACTIVATE: + if (!port->atcphy) + return -ENODEV; + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &capacity); + if (ret || capacity.dp.lanes < 2) + return ret ?: -EINVAL; + return dcp_direct_crossbar_link(dcp, true); + case DPTX_APCALL_DEACTIVATE: + return dcp_direct_crossbar_link(dcp, false); + case DPTX_APCALL_WILL_CHANGE_LINKG_CONFIG: + memset(reply, 0, room); + return dcp_direct_crossbar_link(dcp, false); + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + memset(reply, 0, room); + mdelay(10); + return port->link_rate ? dcp_direct_crossbar_link(dcp, true) : 0; + case DPTX_APCALL_GET_MAX_DRIVE_SETTINGS: + if (room < 28) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(3, reply + 20); + put_unaligned_le32(3, reply + 24); + return 0; + case DPTX_APCALL_SET_DRIVE_SETTINGS: + return -EOPNOTSUPP; + case DPTX_APCALL_GET_DRIVE_SETTINGS: + return dptx_j700_training(service, false, data, size, reply, room); + case DPTX_APCALL_GET_MAX_LINK_RATE: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(LINK_RATE_HBR2, reply + 16); + return 0; + case DPTX_APCALL_GET_LINK_RATE: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(port->link_rate, reply + 16); + return 0; + case DPTX_APCALL_SET_LINK_RATE: + if (size < 32 || room < 32) + return -EINVAL; + switch (get_unaligned_le32(data + 16)) { + case 0: + case LINK_RATE_RBR: + case LINK_RATE_HBR: + case LINK_RATE_HBR2: + case LINK_RATE_HBR3: + break; + default: + return -EINVAL; + } + ret = dcp_direct_crossbar_link(dcp, false); + if (ret) + return ret; + return dptxport_call_set_link_rate(service, data, size, reply, room); + case DPTX_APCALL_GET_MAX_LANE_COUNT: + if (room < 32 || !port->atcphy) + return -EINVAL; + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &capacity); + if (ret || capacity.dp.lanes < 2 || capacity.dp.lanes > 4) + return ret ?: -EINVAL; + port->native26_max_lanes = capacity.dp.lanes; + memset(reply, 0, room); + put_unaligned_le64(capacity.dp.lanes, reply + 16); + return 0; + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: { + u64 lanes; + + if (size < 32 || room < 32) + return -EINVAL; + lanes = get_unaligned_le64(data + 16); + ret = dptx_j700_validate_lanes(port, lanes); + if (ret) + return ret; + port->lane_count = lanes; + put_unaligned_le64(lanes, reply + 16); + if (lanes) + complete(&port->linkcfg_completion); + return 0; + } + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le64(port->lane_count, reply + 16); + return 0; + case DPTX_APCALL_GET_SUPPORTS_HPD: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(1, reply + 16); + return 0; + case DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + return 0; + case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: + if (room < 4) + return -EINVAL; + put_unaligned_le32(1, reply); + return 0; + case DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE: + memset(reply, 0, room); + return 0; + default: + return dptxport_call(service, command, data, size, reply, room); + } +} + static int dptxport_v2_group_call(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t size, void *reply, size_t room) { + if (dcp_is_ext26(service->ep->dcp)) + return dptxport_native26_call(service, group, command, data, size, reply, room); if (group == 1 && command == DPTX_APCALL_SET_DRIVE_SETTINGS) return dptx_j700_training(service, true, data, size, reply, room); if (group == 1 && command == DPTX_APCALL_GET_DRIVE_SETTINGS) @@ -1079,8 +1211,61 @@ static void dptx_controller_service_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { complete(&service->ep->dcp->dptx_controller_ready); + if (dcp_is_ext26(service->ep->dcp) && + dptx_native_controllers_ready(service->ep->dcp)) { + dev_dbg(service->ep->dcp->dev, + "native26 all four controller interfaces OPEN; port OPEN enabled\n"); + afk_service_retry(service->ep->dcp->dptxep); + } +} + +bool dptx_native_controllers_ready(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *ep = READ_ONCE(dcp->dptx_controller_ep); + unsigned long flags, mask = 0; + u32 i; + + if (IS_ERR_OR_NULL(ep)) + return false; + for (i = 0; i < min_t(u32, READ_ONCE(ep->num_channels), AFK_MAX_CHANNEL); i++) { + struct apple_epic_service *service = &ep->services[i]; + + spin_lock_irqsave(&service->lock, flags); + if (service->enabled && !service->torndown) + mask |= (unsigned long)service->cookie; + spin_unlock_irqrestore(&service->lock, flags); + } + return mask == 0xf; } +static int dptx_native_controller_prepare(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + unsigned int kind; + + if (!name || !class || strcmp(class, "AppleDCPDPTXController") || + unit < 0 || unit > 1) + return -EINVAL; + if (!strcmp(name, "dcpav-controller-epic")) + kind = 0; + else if (!strcmp(name, "dcpdp-controller-epic")) + kind = 1; + else + return -EINVAL; + /* A bit identifies each unit's AV and DP interface, independently of channel. */ + service->cookie = (void *)(unsigned long)BIT(unit * 2 + kind); + return 0; +} + +static const struct apple_epic_service_ops dptx_native_controller_ops[] = { + { + .name = "AppleDCPDPTXController", + .prepare = dptx_native_controller_prepare, + .init = dptx_controller_service_init, + }, + {} +}; + static const struct apple_epic_service_ops dptx_controller_ops[] = { { .name = "AppleDCPDPTXController", @@ -1095,17 +1280,20 @@ int dptx_controller_init(struct apple_dcp *dcp) int ret; init_completion(&dcp->dptx_controller_ready); - ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, dptx_controller_ops); + ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, + dcp_is_ext26(dcp) ? dptx_native_controller_ops : dptx_controller_ops); if (IS_ERR(ep)) return PTR_ERR(ep); /* Controller interfaces require the same OPEN handshake as the port. */ - ep->wire_v2 = dcp->hw.iomfb_method_profile == + ep->wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; dcp->dptx_controller_ep = ep; ret = afk_start(ep); if (ret) return ret; + if (dcp_is_ext26(dcp)) + return 0; /* Native OPEN follows A401. */ if (!wait_for_completion_timeout(&dcp->dptx_controller_ready, msecs_to_jiffies(1000))) return -ETIMEDOUT; @@ -1116,7 +1304,7 @@ int dptx_controller_init(struct apple_dcp *dcp) int dptxep_init(struct apple_dcp *dcp) { struct apple_dcp_afkep *ep; - bool wire_v2 = dcp->hw.iomfb_method_profile == + bool wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; int ret; u32 port; @@ -1127,7 +1315,7 @@ int dptxep_init(struct apple_dcp *dcp) init_completion(&dcp->dptxport[0].linkcfg_completion); init_completion(&dcp->dptxport[1].linkcfg_completion); - ep = afk_init(dcp, wire_v2 ? DPTX_ENDPOINT_V2 : DPTX_ENDPOINT, dptxep_ops); + ep = afk_init(dcp, wire_v2 && !dcp_is_ext26(dcp) ? DPTX_ENDPOINT_V2 : DPTX_ENDPOINT, dptxep_ops); if (IS_ERR(ep)) return PTR_ERR(ep); ep->wire_v2 = wire_v2; @@ -1137,6 +1325,8 @@ int dptxep_init(struct apple_dcp *dcp) if (ret) return ret; + if (dcp_is_ext26(dcp)) + return 0; /* Native OPEN follows A401; publication retries discovery. */ for (port = 0; port < dcp->hw.num_dptx_ports; port++) { ret = wait_for_completion_timeout(&dcp->dptxport[port].enable_completion, timeout); @@ -1147,7 +1337,7 @@ int dptxep_init(struct apple_dcp *dcp) * kIOReturnNoDevice (0xe00002c0). Leave the endpoint * up so a later announce can still bind. */ - if (dcp_fw_is_14_7(dcp)) { + if (dcp_fw_is_14_7(dcp) || dcp_is_ext26(dcp)) { dev_info(dcp->dev, "DPTX port %u not announced yet\n", port); return 0; diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 211e72d9855ea8..b577d2ec8ba1af 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -60,6 +60,7 @@ struct dptx_port { struct phy *atcphy; struct mux_control *mux; u32 lane_count; + u32 native26_max_lanes; u32 link_rate, pending_link_rate; u32 drive_settings[2]; u32 trained_voltage[4], trained_pre[4]; @@ -84,5 +85,6 @@ int dptxport_set_hpd_timeout(struct apple_epic_service *service, bool hpd, int dptxport_sink_irq(struct apple_epic_service *service); struct apple_dcp; +bool dptx_native_controllers_ready(struct apple_dcp *dcp); int dpav_ctrl_init(struct apple_dcp *dcp); #endif diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index ed94c48c80d1a4..b551e2f0be5a29 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -39,6 +39,9 @@ static bool dcpavserv_identity(struct apple_epic_service *service, const char *n { if (!name || strcmp(name, "dcpav-service-epic")) return false; + if (dcp_is_ext26(service->ep->dcp)) + return service->ep->endpoint == DPAVSERV_ENDPOINT && unit == 0 && + class && !strcmp(class, "DCPDPService") && service->channel; if (!service->ep->wire_v2) return unit == 0; /* H17P's lower-case interface-id is dynamic, not a physical-port unit. */ @@ -134,6 +137,8 @@ static void dcpavserv_init(struct apple_epic_service *service, const char *name, mutex_unlock(&dcp->dcpavserv.lock); if (v2) mutex_unlock(&dcp->hpd_mutex); + if (published && dcp_is_ext26(dcp) && !dcp->ext26_client_open) + WRITE_ONCE(dcp->ext26_edid_tried, false); if (!published) { /* A generation/registry change during OPEN cannot enable stale work. */ afk_service_retire(service); @@ -567,8 +572,8 @@ static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_servi bool dcpavserv_edid_current(struct apple_dcp *dcp, u64 generation) { lockdep_assert_held(&dcp->hpd_mutex); - return dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL || + return (!dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL) || (dcp->typec_cable_connected && generation == dcp->typec_generation && !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->external_physically_absent)); } @@ -578,7 +583,7 @@ bool dcpavserv_edid_service_current(struct apple_dcp *dcp, u64 epoch) lockdep_assert_held(&dcp->dcpavserv.lock); return dcp->dcpavserv.enabled && dcp->dcpavserv.service && epoch == dcp->dcpavserv.epoch && - (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL || + ((!dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) || !READ_ONCE(dcp->external_physically_absent)); } @@ -641,7 +646,7 @@ const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *dcp, u64 generation edid = service->ep->wire_v2 ? dcpavserv_read_edid_inline(service) : dcpavserv_read_edid_legacy(service); /* The 14.7 external role: every block's header and checksum. */ - if (v14_7_ext && !IS_ERR(edid) && !drm_edid_valid(edid)) { + if ((v14_7_ext || dcp_is_ext26(dcp)) && !IS_ERR(edid) && !drm_edid_valid(edid)) { dev_warn(dcp->dev, "DCPAVSERV: the sink's EDID is not valid\n"); drm_edid_free(edid); edid = ERR_PTR(-EPROTO); @@ -700,7 +705,7 @@ int dpavservep_init(struct apple_dcp *dcp) if (IS_ERR(ep)) return PTR_ERR(ep); /* External H17P uses AFKV2 service calls with host-owned OOB buffers. */ - ep->wire_v2 = dcp->hw.iomfb_method_profile == + ep->wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; ep->inline_v2 = false; dcp->dcpavservep = ep; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6e80eca48ad9ac..897d1520a33b97 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -36,6 +36,7 @@ #include "dcp-internal.h" #include "iomfb.h" #include "iomfb_internal.h" +#include "dcpext26-lifecycle.h" #include "parser.h" #include "trace.h" @@ -162,6 +163,8 @@ static u8 dcp_pop_depth(u8 *depth) } /* Older firmware and the H17G method profile retain their existing transport. */ +static u64 ext26_scanout_bytes(struct iomfb_scanout_h17p *scanout); + static bool iomfb_uses_queue(struct apple_dcp *dcp) { return dcp->fw_compat == DCP_FIRMWARE_H17P && @@ -197,7 +200,7 @@ static int iomfb_enqueue_opaque_x(struct apple_dcp *dcp) lockdep_assert_held(&dcp->iomfb.lock); /* DCPEXT rejects enableGPAlphaDiv; retain the panel-only workaround. */ if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(dcp)) return 0; if (atomic_read(&dcp->iomfb.opaque_x_state) != IOMFB_OPAQUE_X_NEEDED) return 0; @@ -243,7 +246,8 @@ static void iomfb_queue_advance(struct apple_dcp *dcp) /* The active operation may still be visible to firmware. */ return; } - if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending) + if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending || + dcp->ext26_control_pending) return; if (transaction) { @@ -648,6 +652,8 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G && call->tag_h17g[0]) memcpy(resolved.tag, call->tag_h17g, sizeof(resolved.tag)); + if (dcp_is_ext26(dcp) && call->tag_ext26[0]) + memcpy(resolved.tag, call->tag_ext26, sizeof(resolved.tag)); call = &resolved; enum dcp_context_id context = dcp_call_context(dcp, oob); @@ -688,9 +694,9 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca if (in_len > 0) memcpy(out_data, data, in_len); - /* An unwritten status must not look like a successful response. */ + /* Native26 Boolean replies may leave their padding unwritten. */ if (out_len) - memset(out_data + in_len, 0xff, out_len); + memset(out_data + in_len, dcp_is_ext26(dcp) ? 0 : 0xff, out_len); trace_iomfb_push(dcp, call, context, offset, depth); @@ -700,8 +706,19 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca ch->header[depth] = header; ch->end[depth] = offset + ALIGN(data_len, DCP_PACKET_ALIGNMENT); + if (dcp_is_ext26(dcp)) + dma_wmb(); dcp_send_message(dcp, IOMFB_ENDPOINT, dcpep_msg(context, data_len, offset)); + /* Only a successful transport send can establish the readiness boundary. */ + if (dcp_is_ext26(dcp) && !memcmp(call->tag, "A473", 4) && in_len == 16 && + get_unaligned_le64(data) == 1) { + bool sent = !READ_ONCE(dcp->crashed) && !READ_ONCE(dcp->quiescing) && + !READ_ONCE(dcp->ext26_blocked); + + WRITE_ONCE(dcp->ext26_power_ns, sent ? ktime_get_ns() : 0); + WRITE_ONCE(dcp->ext26_power_sent, sent); + } } /* Parse a callback tag "D123" into the ID 123. Returns -EINVAL on failure. */ @@ -729,6 +746,8 @@ void dcp_ack(struct apple_dcp *dcp, enum dcp_context_id context) struct dcp_channel *ch = dcp_get_channel(dcp, context); dcp_pop_depth(&ch->depth); + if (dcp_is_ext26(dcp)) + dma_wmb(); dcp_send_message(dcp, IOMFB_ENDPOINT, dcpep_ack(context)); } @@ -1293,6 +1312,13 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, out = in + hdr->in_len; if (!handled) { + if (dcp_is_ext26(dcp)) { + dev_err(dev, "native26 unsupported callback D%03d (%u/%u); session retained\n", + tag, hdr->in_len, hdr->out_len); + WRITE_ONCE(dcp->crashed, true); + complete(&dcp->start_done); + return; + } zero_reply = dcp_external_zero_reply(dcp, tag, context, hdr->in_len, hdr->out_len); if (!zero_reply && @@ -1374,15 +1400,16 @@ static void dcpep_handle_ack(struct apple_dcp *dcp, enum dcp_context_id context) * acked on the callback context, whose receive area holds the callback * record rather than the reply, so the ack cannot be used to find it. * - * On older firmware the lengths in the record header must still be - * the ones that were sent; otherwise the record was overwritten and - * its output cannot be trusted. The tag is left out of the check: it - * has only been seen preserved on 13.5 firmware. + * Native26 preserves the command header, including its tag. Validate it + * before using the reply. Older firmware keeps its length-only check; + * other H17 profiles retain their existing admission policy. */ - if (dcp->fw_compat != DCP_FIRMWARE_H17P) { + if (dcp->fw_compat != DCP_FIRMWARE_H17P || dcp_is_ext26(dcp)) { record = out - sent->in_len - sizeof(*record); if (record->in_len != sent->in_len || - record->out_len != sent->out_len) { + record->out_len != sent->out_len || + (dcp_is_ext26(dcp) && + memcmp(record->tag, sent->tag, sizeof(record->tag)))) { dev_err(dcp->dev, "corrupt %c%c%c%c command record on context %X\n", sent->tag[3], sent->tag[2], sent->tag[1], @@ -1421,6 +1448,8 @@ static void dcpep_got_msg(struct apple_dcp *dcp, u64 message) } data = dcp->shmem + channel_offset + offset; + if (dcp_is_ext26(dcp)) + dma_rmb(); if (FIELD_GET(IOMFB_MSG_ACK, message)) dcpep_handle_ack(dcp, ctx_id); @@ -1799,7 +1828,7 @@ int dcp_get_modes(struct drm_connector *connector) dcp = platform_get_drvdata(pdev); modes = dcp_modes_snapshot(dcp, apple_connector, &nr_modes); - physical_edid = dcp->hw.iomfb_method_profile == + physical_edid = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; /* get_modes holds mode_config.mutex; HPD paths never acquire it. */ if (physical_edid) { @@ -1822,6 +1851,8 @@ int dcp_get_modes(struct drm_connector *connector) /* Internal H17P still has no supported EDID command transport. */ if (nr_modes && !apple_connector->drm_edid && + (!dcp_is_ext26(dcp) || !dcp->ext26_edid_tried || + dcp->ext26_edid_generation != edid_generation) && (dcp->fw_compat != DCP_FIRMWARE_H17P || (dcp->dcpavservep && dcp->dcpavservep->wire_v2))) { const struct drm_edid *edid; @@ -1829,6 +1860,10 @@ int dcp_get_modes(struct drm_connector *connector) mutex_unlock(&dcp->dcpavserv.lock); if (physical_edid) mutex_unlock(&dcp->hpd_mutex); + if (dcp_is_ext26(dcp)) { + dcp->ext26_edid_tried = true; + dcp->ext26_edid_generation = edid_generation; + } edid = dcpavserv_copy_edid(dcp, edid_generation, &service_epoch); if (physical_edid) mutex_lock(&dcp->hpd_mutex); @@ -1851,6 +1886,15 @@ int dcp_get_modes(struct drm_connector *connector) /* Retirement may have queued cleanup while this request waited. */ dcp_consume_edid_invalidation(apple_connector); if (IS_ERR_OR_NULL(edid)) { + if (dcp_is_ext26(dcp) && IS_ERR(edid) && + PTR_ERR(edid) != -ENODEV && PTR_ERR(edid) != -ESTALE) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + apple_connector->connected = false; + nr_modes = 0; + dev_err(dcp->dev, "native26 initial EDID discovery failed: %pe; session retained\n", edid); + goto edid_done; + } /* * There is no AV service for an internal panel, nor * for an external display while its link is down; @@ -1922,6 +1966,13 @@ int dcp_get_modes(struct drm_connector *connector) } edid_done: + /* Discovery can replace the catalogue while the OOB read waits. */ + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->crashed) || READ_ONCE(dcp->ext26_blocked)) + nr_modes = 0; + else + modes = dcp_modes_snapshot(dcp, apple_connector, &nr_modes); + } /* Publish after a possibly delayed raw read, under its physical generation. */ for (i = 0; i < nr_modes; ++i) { mode = drm_mode_duplicate(dev, &modes[i].mode); @@ -2028,7 +2079,7 @@ static bool dcp_mode_settable(struct apple_dcp *dcp, if (!lookup_mode(dcp, mode, &dcp_mode)) return false; - if (dcp->fw_compat == DCP_FIRMWARE_H17P && + if (dcp->fw_compat == DCP_FIRMWARE_H17P && !dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) return dcp_mode.mode.type & DRM_MODE_TYPE_PREFERRED; @@ -2065,7 +2116,7 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, /* Disconnect cannot pass the admission check before these RPCs finish. */ guard(mutex)(&dcp->external_lock); - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && + if (dcp_is_retained_external(dcp) && (!dcp->external_running || dcp->external_error || READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->crashed) || READ_ONCE(dcp->iomfb.presentation_paused))) { /* @@ -2113,7 +2164,8 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, ret = iomfb_v14_7_modeset(dcp, crtc_state); break; case DCP_FIRMWARE_H17P: - ret = iomfb_modeset_h17p(dcp, crtc_state); + ret = iomfb_modeset_h17p(dcp, crtc_state, + drm_atomic_get_new_plane_state(commit, crtc->primary)); break; default: WARN_ONCE(true, "Unexpected firmware version: %u\n", @@ -2455,6 +2507,12 @@ VISIBLE_IF_KUNIT void iomfb_scanout_put_h17p(struct apple_dcp *dcp, return; if (dcp->iomfb.next_scanout == scanout) dcp->iomfb.next_scanout = NULL; + if (dcp_is_ext26(dcp)) { + /* An unsuccessful present can still have exposed its IOVAs. */ + list_add_tail(&scanout->link, &dcp->ext26_retained); + dcp->ext26_retained_bytes += ext26_scanout_bytes(scanout); + return; + } iomfb_scanout_release_h17p(scanout); } @@ -2692,7 +2750,12 @@ static void iomfb_recv_message(struct apple_dcp *dcp, u64 message) "IOMFB: init complete %#llx (version %llu)\n", message, FIELD_GET(GENMASK_ULL(63, 48), message)); - iomfb_start(dcp); + if (dcp_is_ext26(dcp)) { + dev_dbg(dcp->dev, "native26 DCPLink ready before AFK startup\n"); + complete_all(&dcp->ext26_link_ready); + } else { + iomfb_start(dcp); + } } else if (type == IOMFB_MESSAGE_TYPE_MSG) dcpep_got_msg(dcp, message); else @@ -2715,6 +2778,15 @@ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message) mutex_unlock(&dcp->iomfb.lock); } +void iomfb_ext26_start(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp->iomfb.lock); + WRITE_ONCE(dcp->iomfb.owner, current); + iomfb_start(dcp); + WRITE_ONCE(dcp->iomfb.owner, NULL); + iomfb_queue_advance(dcp); +} + int iomfb_start_rtkit(struct apple_dcp *dcp) { dma_addr_t shmem_iova; @@ -2737,7 +2809,8 @@ int iomfb_start_rtkit(struct apple_dcp *dcp) * display DART faults. */ if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) { + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL && + !dcp_is_ext26(dcp)) { ret = apple_rtkit_start_ep(dcp->rtk, REMOTE_ALLOC_ENDPOINT); if (ret) return ret; @@ -2784,3 +2857,61 @@ void iomfb_shutdown(struct apple_dcp *dcp) #if IS_ENABLED(CONFIG_DRM_APPLE_PARSER_KUNIT_TEST) #include "iomfb-parser-test.c" #endif + +static u64 ext26_scanout_bytes(struct iomfb_scanout_h17p *scanout) +{ + u64 bytes = 0; + unsigned int i; + + for (i = 0; i < SWAP_SURFACES; i++) + if (scanout->fb[i] && scanout->fb[i]->obj[0]) + bytes += scanout->fb[i]->obj[0]->size; + return bytes; +} + +/* A release/transport boundary is not a backing-retirement witness. */ +int iomfb_ext26_retain_epoch(struct apple_dcp *dcp) +{ + struct iomfb_scanout_h17p *scanout; + u64 bytes = 0; + + guard(mutex)(&dcp->iomfb.lock); + if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending || + dcp->iomfb.active || dcp->iomfb.next_scanout || dcp->chunks.data || + dcp->ext26_control_pending) + return -EBUSY; + if (dcp->iomfb.scanout) + bytes += ext26_scanout_bytes(dcp->iomfb.scanout); + list_for_each_entry(scanout, &dcp->iomfb.dropped, link) + bytes += ext26_scanout_bytes(scanout); + if (!dcpext26_epoch_fits(dcp->ext26_epochs, dcp->ext26_retained_bytes, bytes)) + return -ENOSPC; + if (dcp->iomfb.scanout) { + list_add_tail(&dcp->iomfb.scanout->link, &dcp->ext26_retained); + dcp->iomfb.scanout = NULL; + } + list_splice_tail_init(&dcp->iomfb.dropped, &dcp->ext26_retained); + dcp->ext26_retained_bytes += bytes; + if (bytes) + dcp->ext26_epochs++; + dcp->ext26_blanked = false; + dcp->ext26_normal = false; + dcp->ext26_early_complete = false; + dev_dbg(dcp->dev, "native26 retained epoch %u: %llu backing bytes; remote/DMA fence unproved\n", + dcp->ext26_epochs, dcp->ext26_retained_bytes); + return 0; +} + +/* A retained old attachment may still address this GEM object. */ +bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp, struct drm_framebuffer *fb) +{ + struct iomfb_scanout_h17p *scanout; + unsigned int i; + + guard(mutex)(&dcp->iomfb.lock); + list_for_each_entry(scanout, &dcp->ext26_retained, link) + for (i = 0; i < SWAP_SURFACES; i++) + if (scanout->fb[i] && scanout->fb[i]->obj[0] == fb->obj[0]) + return false; + return true; +} diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 25c2e0254183f9..4b23cb0591f88c 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -204,8 +204,13 @@ struct dcp_method_entry { char tag[4]; /* H17G shares the H17P transport but changes selected method IDs. */ char tag_h17g[4]; + char tag_ext26[4]; }; +#define IOMFB_METHOD_EXT26(tag, native, name) [name] = { #name, \ + { tag[0], tag[1], tag[2], tag[3] }, {0}, \ + { native[0], native[1], native[2], native[3] } } + #define IOMFB_MAX_CB (1000) bool iomfb_validate_callback_h17p(int tag, u32 in_len, u32 out_len); diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 4f13cee71c8720..b963642fec05a3 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -2,9 +2,13 @@ /* Copyright The Asahi Linux Contributors */ #include +#include +#include #include #include "dcp-link.h" +#include "dcpext26-wire.h" +#include "dcpext26-lifecycle.h" #include "iomfb_h17p.h" #include "iomfb_v12_3.h" #include "iomfb_v13_3.h" @@ -196,11 +200,12 @@ void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { IOMFB_METHOD("A000", dcpep_late_init_signal), - IOMFB_METHOD_H17("A025", "A029", dcpep_setup_video_limits), /* (0,0) */ + [dcpep_setup_video_limits] = { "dcpep_setup_video_limits", + {'A', '0', '2', '5'}, {'A', '0', '2', '9'}, {'A', '0', '2', '9'} }, /* (0,0) */ IOMFB_METHOD("A131", iomfbep_a131_pmu_service_matched), /* nested in D206 */ IOMFB_METHOD("A132", iomfbep_a132_backlight_service_matched), /* nested in D207 */ - IOMFB_METHOD("A385", dcpep_set_create_dfb), /* first call inside D121; (0,0) */ - IOMFB_METHOD("A386", iomfbep_a358_vi_set_temperature_hint), /* nested in D100 */ + IOMFB_METHOD_EXT26("A385", "A389", dcpep_set_create_dfb), /* first call inside D121; (0,0) */ + IOMFB_METHOD_EXT26("A386", "A390", iomfbep_a358_vi_set_temperature_hint), /* nested in D100 */ IOMFB_METHOD("A401", dcpep_start_signal), IOMFB_METHOD("A406", dcpep_swap_start), /* @@ -208,7 +213,7 @@ static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { * with it: every swap is A406 followed by A408, A407 is unused, and the * A408 response is 0x0c bytes (dcp_swap_submit_resp_h17p). */ - IOMFB_METHOD("A408", dcpep_swap_submit), + IOMFB_METHOD_EXT26("A408", "A407", dcpep_swap_submit), IOMFB_METHOD("A411", dcpep_set_display_device), IOMFB_METHOD("A412", dcpep_is_main_display), IOMFB_METHOD("A413", dcpep_set_digital_out_mode), @@ -253,6 +258,13 @@ static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { #define DCP_FW h17p #define DCP_FW_VER DCP_FW_VERSION(26, 6, 0) +static int ext26_encode(struct apple_dcp *dcp, struct dcp_swap_submit_req_h17p *request); +static int ext26_activate(struct apple_dcp *dcp); +static int ext26_open_client(struct apple_dcp *dcp); +static u8 ext26_prop_start(struct apple_dcp *dcp, u32 advertised); +static void ext26_start(struct apple_dcp *dcp); +static bool ext26_abort(struct apple_dcp *dcp, int tag, void *out, void *input); + #include "iomfb_template.c" void iomfb_apply_opaque_x_h17p(struct apple_dcp *dcp, dcp_callback_t callback, @@ -699,10 +711,465 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, /* find_swap_function_gated */ }; +/* Included with the public H17P template helpers in scope. */ + +static bool ext26_callback_size(int tag, u32 input, u32 output) +{ + switch (tag) { + case 3: return input == 4 && output == 60; + case 6: return input == 84 && output == 80; + case 107: return input == 64 && !output; + case 115: return input == 16 && output == 8; + case 116: return input == 16 && output == 4; + case 134: return !input && output == 4; + case 408: return input == 8 && output == 8; + case 454: return input == 4 && output == 4; + case 567: return input == 128 && output == 4; + case 572: return input == 4 && output == 4; + case 576: return input == 4 && !output; + case 588: return !input && !output; + default: return iomfb_validate_callback_h17p(tag, input, output); + } +} + +static u8 ext26_prop_start(struct apple_dcp *dcp, u32 advertised) +{ + struct dcp_chunks *chunks = &dcp->chunks; + + if (!advertised || advertised > SZ_1M + 1) + return false; + kfree(chunks->data); + memset(chunks, 0, sizeof(*chunks)); + chunks->length = advertised - 1; + chunks->contiguous = true; + chunks->modes_generation = dcp_modes_transfer_begin(dcp); + dcp_firmware_metadata_begin(dcp); + chunks->data = kzalloc(max_t(size_t, 1, chunks->length), GFP_KERNEL); + return !!chunks->data; +} + +struct ext26_boot { + unsigned int completed; + u32 one; +}; + +static void ext26_boot_next(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct ext26_boot *boot = cookie; + const struct dcp_method_entry *method; + u32 in = 0, reply = 0; + + if (READ_ONCE(dcp->crashed) || (boot->completed && !out) || + ((boot->completed == 5 || boot->completed == 6) && + get_unaligned_le32(out) != 1)) { + WRITE_ONCE(dcp->crashed, true); + dev_err(dcp->dev, + "native26 D121 startup refused: completed=%u last=%s reply=%#x present=%u; session retained\n", + boot->completed, boot->completed == 5 ? "A000" : + boot->completed == 6 ? "A465" : "earlier call", + out && (boot->completed == 2 || boot->completed == 5 || + boot->completed == 6) ? get_unaligned_le32(out) : 0, !!out); + kfree(boot); + return; + } + if (boot->completed == 5 || boot->completed == 6) + dev_dbg(dcp->dev, "native26 D121 %s reply=%#x accepted\n", + boot->completed == 5 ? "A000" : "A465", get_unaligned_le32(out)); + switch (boot->completed++) { + case 0: + method = &dcp_methods[dcpep_set_create_dfb]; + break; + case 1: + method = &dcp_methods[dcpep_create_default_fb]; + reply = 4; + break; + case 2: + method = &dcp_methods[dcpep_setup_video_limits]; + break; + case 3: + method = &dcp_methods[dcpep_flush_supports_power]; + in = 4; + break; + case 4: + method = &dcp_methods[dcpep_late_init_signal]; + in = reply = 4; + break; + case 5: + method = &dcp_methods[dcpep_set_display_refresh_properties]; + reply = 4; + break; + default: + kfree(boot); + boot_done(dcp, NULL, NULL); + dev_dbg(dcp->dev, "native26 D121 startup complete\n"); + return; + } + dcp_push(dcp, false, method, in, reply, &boot->one, ext26_boot_next, boot); +} + +static bool ext26_boot_callback(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + struct ext26_boot *boot = kzalloc_obj(*boot); + + if (!boot) { + WRITE_ONCE(dcp->crashed, true); + return false; + } + boot->one = 1; + ext26_boot_next(dcp, NULL, boot); + return false; +} + +static bool ext26_property(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + if (tag == 107 && (!strncmp((const char *)in, "TimingElements", 64) || + !strncmp((const char *)in, "DisplayAttributes", 64))) { + dcp_modes_begin_attachment(dcp); + if (dcp->connector) { + dcp->connector->connected = false; + apple_connector_invalidate_edid(dcp->connector); + dcp_queue_hotplug(dcp->connector); + } + } + if (memcmp(in, "NormalModeActive", sizeof("NormalModeActive"))) + return true; + if (tag == 107) { + WRITE_ONCE(dcp->ext26_normal, false); + return true; + } + bool on = tag == 104 ? !!in[64] : !!get_unaligned_le32(in + 64); + + if (!on) { + WRITE_ONCE(dcp->ext26_normal, false); + return true; + } + if (READ_ONCE(dcp->ext26_power_sent) && + dcp->ext26_rx_ns > READ_ONCE(dcp->ext26_power_ns) && + dcp->ext26_generation == READ_ONCE(dcp->typec_generation)) { + WRITE_ONCE(dcp->ext26_normal, true); + complete_all(&dcp->ext26_ready); + dev_dbg(dcp->dev, "native26 NormalModeActive after power publication\n"); + } + return true; +} + +static bool ext26_bandwidth(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + memset(out, 0, 60); + put_unaligned_le32(1, (u8 *)out + 56); + return true; +} + +static bool ext26_frame_sync(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + memset(out, 0, 80); + if (!(in[80] & 1)) + memcpy(out, in, 80); + return true; +} + +static bool ext26_defaults(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u32 count = get_unaligned_le32(in + 64); + + memset(out, 0, 36); + if (count <= 8) + memcpy(out, in + 68, count * 4); + return true; +} + +static bool ext26_clock(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u32 index = get_unaligned_le32(in + 4); + u64 rate = 0; + + if (!memcmp(in, "VORP", 4) && index < 2) + rate = index ? 24000000 : 48000000; + put_unaligned_le64(rate, out); + return true; +} + +static bool ext26_analytics(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + memset(out, 0, 4100); + if (memchr(in, 0, 64) && !in[4160]) + memcpy(out, in + 64, 4096); + return true; +} + +static bool ext26_tiling(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + memset(out, 0, 8); + if (((u8 *)input)[12] <= 1) + ((u8 *)out)[4] = 1; + return true; +} + +static bool ext26_release(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + u32 id = get_unaligned_le32(in); + + memset(out, 0, 4); + /* Logical descriptor release never proves DMA retirement. */ + if (id && id < DCP_MAX_MAPPINGS && test_bit(id, dcp->memdesc_map) && + !test_and_set_bit(id, dcp->ext26_released)) + *(u8 *)out = 1; + return true; +} + +static bool ext26_allocate(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 bytes = get_unaligned_le64(in + 4); + u32 alignment = get_unaligned_le32(in + 12); + + memset(out, 0, 28); + if (get_unaligned_le32(in) != 0x703 || !is_power_of_2(alignment) || + alignment > SZ_16K || in[16] || in[17] || in[18] || !bytes || + bytes > SZ_64M || ALIGN(bytes, SZ_16K) > SZ_256M - dcp->ext26_buffer_bytes) + return true; + trampoline_allocate_buffer_h17p(dcp, tag, out, input); + if (get_unaligned_le32((u8 *)out + 24)) + dcp->ext26_buffer_bytes += ALIGN(bytes, SZ_16K); + return true; +} + +static bool ext26_hotplug(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 connected = get_unaligned_le64(in); + + memset(out, 0, 76); + if (!(in[84] & 1)) + memcpy(out, in + 8, 76); + + dcpep_cb_hotplug(dcp, &connected); + return true; +} + +static void ext26_started(struct apple_dcp *dcp, void *out, void *cookie) +{ + if (!out || READ_ONCE(dcp->crashed) || get_unaligned_le32(out) != 1) { + WRITE_ONCE(dcp->crashed, true); + complete_all(&dcp->start_done); + return; + } + dcp_ext26_open_services(dcp); + dcp->main_display = false; + dcp->active = true; + dev_dbg(dcp->dev, "native26 A401 complete; service OPEN and route admission enabled\n"); + complete_all(&dcp->start_done); +} + +static void ext26_client_reply(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_wait_cookie *wait = cookie; + + WRITE_ONCE(wait->replied, !!out); + complete(&wait->done); + kref_put(&wait->refcount, release_wait_cookie); +} + +static int ext26_open_client(struct apple_dcp *dcp) +{ + struct dcp_wait_cookie *wait; + int ret; + + if (dcp->ext26_client_open) + return 0; + wait = kzalloc_obj(*wait); + if (!wait) + return -ENOMEM; + init_completion(&wait->done); + kref_init(&wait->refcount); + kref_get(&wait->refcount); + dcp_first_client_open(dcp, false, ext26_client_reply, wait); + ret = wait_for_completion_timeout(&wait->done, msecs_to_jiffies(5000)) && + READ_ONCE(wait->replied) && !READ_ONCE(dcp->crashed) ? 0 : -ETIMEDOUT; + kref_put(&wait->refcount, release_wait_cookie); + if (!ret) { + dcp->ext26_client_open = true; + dev_dbg(dcp->dev, "native26 A457 complete before first mode\n"); + } else { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + } + return ret; +} + +static void ext26_main_done(struct apple_dcp *dcp, void *out, void *cookie) +{ + if (!out) { + WRITE_ONCE(dcp->crashed, true); + return; + } + dcp_ack(dcp, DCP_CONTEXT_CB); +} + +static bool ext26_main_query(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + dcp_is_main_display(dcp, false, ext26_main_done, NULL); + return false; +} + +static bool ext26_stride(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + put_unaligned_le32(dcp->ext26_stride, out); + return true; +} + +static bool ext26_match_backlight(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + iomfb_a132_backlight_service_matched(dcp, false, complete_backlight_service_matched, out); + return false; +} + +static bool ext26_map_piodma(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 id = get_unaligned_le64(in); + + memset(out, 0, 16); + if (!id || id >= DCP_MAX_MAPPINGS || get_unaligned_le32(in + 8) != 1 || + !test_bit(id, dcp->memdesc_map) || test_bit(id, dcp->ext26_released)) + return true; + if (dcp->memdesc[id].piodma_mapped) { + put_unaligned_le32(id, out); + put_unaligned_le64(dcp->memdesc[id].dva, (u8 *)out + 4); + return true; + } + return trampoline_map_piodma_h17p(dcp, tag, out, input); +} + +static void ext26_start(struct apple_dcp *dcp) +{ + iomfb_cb_handler *handlers = kcalloc(IOMFB_MAX_CB, sizeof(*handlers), GFP_KERNEL); + + if (!handlers) { + WRITE_ONCE(dcp->crashed, true); + complete(&dcp->start_done); + return; + } + memcpy(handlers, cb_handlers, sizeof(cb_handlers)); + handlers[0] = handlers[112] = trampoline_true; + handlers[3] = ext26_bandwidth; + handlers[6] = ext26_frame_sync; + handlers[102] = handlers[104] = handlers[107] = ext26_property; + handlers[114] = ext26_analytics; + handlers[115] = ext26_tiling; + handlers[116] = handlers[134] = trampoline_zero; + handlers[588] = trampoline_nop; + handlers[121] = ext26_boot_callback; + handlers[101] = ext26_stride; + handlers[125] = ext26_defaults; + handlers[201] = ext26_map_piodma; + handlers[207] = ext26_match_backlight; + handlers[130] = handlers[411] = handlers[452] = trampoline_zero; + handlers[202] = handlers[592] = handlers[594] = trampoline_nop; + handlers[400] = handlers[401] = handlers[574] = trampoline_zero; + handlers[408] = ext26_clock; + handlers[451] = ext26_allocate; + handlers[454] = ext26_release; + handlers[575] = ext26_hotplug; + handlers[582] = trampoline_zero; + handlers[593] = ext26_abort; + handlers[599] = ext26_main_query; + dcp->ext26_handlers = handlers; + dcp->cb_handlers = handlers; + dcp_start_signal(dcp, false, ext26_started, NULL); +} + +static int ext26_encode(struct apple_dcp *dcp, struct dcp_swap_submit_req_h17p *request) +{ + struct dcpext26_primary primary, *selected = NULL; + const struct dcp_swap_submit_req_h17p *effective = request; + const struct dcp_surface *surface; + struct iomfb_scanout_h17p *scanout = dcp->iomfb.next_scanout; + struct drm_framebuffer *fb = scanout ? scanout->fb[0] : NULL; + + if (READ_ONCE(dcp->ext26_blocked) || !READ_ONCE(dcp->ext26_normal)) + return -EPIPE; + if (!(request->swap.swap_enabled & BIT(0)) && dcp->iomfb.scanout) + effective = &dcp->iomfb.scanout->request; + surface = &effective->surf[0].base; + if (!effective->surf_null[0]) { + if (!fb || !fb->obj[0]) + return -EINVAL; + primary = (struct dcpext26_primary) { + .address = effective->surf_iova[0], .backing_bytes = fb->obj[0]->size, + .width = surface->width, .height = surface->height, .pitch = surface->stride, + }; + selected = &primary; + } + return dcpext26_primary_encode(dcp->ext26_present, sizeof(dcp->ext26_present), + request->swap.swap_id, 0, selected); +} + +static void ext26_power_reply(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_wait_cookie *wait = cookie; + + wait->status = out && get_unaligned_le64(out) == 1 ? 0 : ~0U; + complete(&wait->done); + kref_put(&wait->refcount, release_wait_cookie); +} + +static bool ext26_abort(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + u32 id = get_unaligned_le32(input); + + if (iomfb_ext26_control_complete(dcp, id, true)) + return true; + bool ack = trampoline_abort_swap_ap_gated(dcp, tag, out, input); + + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + return ack; +} + +static int ext26_activate(struct apple_dcp *dcp) +{ + struct dcp_set_power_state_req_h17p request = { .unklong = 1 }; + struct dcp_wait_cookie *wait; + int ret; + + wait = kzalloc_obj(*wait); + if (!wait) + return -ENOMEM; + wait->status = ~0U; + init_completion(&wait->done); + kref_init(&wait->refcount); + kref_get(&wait->refcount); + WRITE_ONCE(dcp->external_power_attempted, true); + dcp_set_power_state(dcp, false, &request, ext26_power_reply, wait); + ret = wait_for_completion_timeout(&wait->done, msecs_to_jiffies(5000)); + ret = ret && !READ_ONCE(wait->status) ? 0 : -EIO; + kref_put(&wait->refcount, release_wait_cookie); + if (!ret && !wait_for_completion_timeout(&dcp->ext26_ready, msecs_to_jiffies(5000))) + ret = -ETIMEDOUT; + if (!ret && (!READ_ONCE(dcp->ext26_normal) || READ_ONCE(dcp->crashed) || + READ_ONCE(dcp->ext26_blocked))) + ret = -EIO; + if (!ret) { + WRITE_ONCE(dcp->pipe_enabled_h17p, true); + dev_dbg(dcp->dev, "native26 mode and power ready; primary scanout admitted\n"); + } + return ret; +} + bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len) { - return iomfb_validate_callback_h17p(tag, in_len, out_len); + return dcp_is_ext26(dcp) ? ext26_callback_size(tag, in_len, out_len) : + iomfb_validate_callback_h17p(tag, in_len, out_len); } static_assert(sizeof(struct dcp_set_power_state_resp) == 8); @@ -785,7 +1252,9 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) init_completion(&cookie->wait.done); kref_init(&cookie->wait.refcount); kref_get(&cookie->wait.refcount); - if (on) { + if (dcp_is_ext26(dcp)) { + dcp_set_power_state(dcp, false, &request, iomfb_external_power_complete, cookie); + } else if (on) { u32 handle = 2; iomfb_opaque_x_reset_h17p(dcp); @@ -793,7 +1262,7 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) } else { dcp_set_power_state(dcp, true, &request, iomfb_external_power_complete, cookie); } - ret = wait_for_completion_timeout(&cookie->wait.done, msecs_to_jiffies(500)); + ret = wait_for_completion_timeout(&cookie->wait.done, msecs_to_jiffies(dcp_is_ext26(dcp) ? 5000 : 500)); ret = ret ? (int)READ_ONCE(cookie->wait.status) : -ETIMEDOUT; kref_put(&cookie->wait.refcount, release_wait_cookie); return ret; @@ -801,6 +1270,10 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) void iomfb_start_h17p(struct apple_dcp *dcp) { + if (dcp_is_ext26(dcp)) { + ext26_start(dcp); + return; + } WRITE_ONCE(dcp->pipe_enabled_h17p, false); dcp->cb_handlers = cb_handlers; dcp_start_signal(dcp, false, dcp_started, NULL); @@ -839,3 +1312,107 @@ int iomfb_h17p_deliver_swap_complete(struct apple_dcp *dcp, u32 swap_id) #undef DCP_FW_VER #undef DCP_FW + +struct ext26_control { + struct dcpext26_exchange exchange; + struct completion done; + u8 wire[DCPEXT26_SUBMIT_BYTES]; +}; + +static void ext26_control_signal(struct apple_dcp *dcp, struct ext26_control *control) +{ + if (dcpext26_exchange_finished(&control->exchange)) { + if (control->exchange.failed) { + WRITE_ONCE(dcp->crashed, true); + WRITE_ONCE(dcp->ext26_blocked, true); + } + dcp->ext26_control_pending = false; + complete_all(&control->done); + } +} + +bool iomfb_ext26_control_complete(struct apple_dcp *dcp, u32 id, bool aborted) +{ + struct ext26_control *control = dcp->ext26_control; + + if (!dcp_is_ext26(dcp)) + return false; + if (id && id == dcp->ext26_control_id) + return true; + if (!control || !dcpext26_exchange_complete(&control->exchange, id)) + return false; + if (aborted) + control->exchange.failed = true; + ext26_control_signal(dcp, control); + return true; +} + +static void ext26_null_submitted(struct apple_dcp *dcp, void *reply, void *cookie) +{ + struct ext26_control *control = cookie; + + control->exchange.replied = true; + if (dcpext26_submit_reply(reply, 12)) + control->exchange.failed = true; + ext26_control_signal(dcp, control); +} + +static void ext26_null_started(struct apple_dcp *dcp, void *reply, void *cookie) +{ + struct ext26_control *control = cookie; + u32 id; + + if (dcpext26_start_reply(reply, 8, &id) || + !dcpext26_exchange_begin(&control->exchange, id) || + dcpext26_primary_encode(control->wire, sizeof(control->wire), id, 0, NULL)) { + control->exchange.failed = true; + ext26_control_signal(dcp, control); + return; + } + dcp_push(dcp, false, &dcp_methods[dcpep_swap_submit], sizeof(control->wire), 12, + control->wire, ext26_null_submitted, control); +} + +/* Called by the lifecycle worker, never from RX or the shared DRM tail. */ +int iomfb_ext26_blank(struct apple_dcp *dcp) +{ + struct ext26_control *control; + u8 start[16] = {}; + int ret; + + ret = iomfb_queue_settle(dcp, msecs_to_jiffies(5000)); + if (ret) + return ret; + control = kzalloc_obj(*control); + if (!control) + return -ENOMEM; + init_completion(&control->done); + mutex_lock(&dcp->iomfb.lock); + if (dcp->ext26_control || dcp->present_state_h17p.pending || dcp->crashed) { + mutex_unlock(&dcp->iomfb.lock); + kfree(control); + return -EBUSY; + } + dcp->ext26_control = control; + dcp->ext26_control_pending = true; + mutex_unlock(&dcp->iomfb.lock); + dcp_push(dcp, false, &dcp_methods[dcpep_swap_start], sizeof(start), 8, + start, ext26_null_started, control); + ret = wait_for_completion_timeout(&control->done, msecs_to_jiffies(5000)); + /* RX no longer owns a successful control before its storage is reclaimed. */ + if (ret) + apple_rtkit_flush_rx(dcp->rtk); + mutex_lock(&dcp->iomfb.lock); + if (!ret || control->exchange.failed || dcp->crashed) { + WRITE_ONCE(dcp->crashed, true); + ret = -EIO; + /* Possibly published control storage remains owned until reset. */ + } else { + dcp->ext26_control_id = control->exchange.id; + dcp->ext26_control = NULL; + kfree(control); + ret = 0; + } + mutex_unlock(&dcp->iomfb.lock); + return ret; +} diff --git a/drivers/gpu/drm/apple/iomfb_internal.h b/drivers/gpu/drm/apple/iomfb_internal.h index bd55bfd475f3ed..11b9b8e220da67 100644 --- a/drivers/gpu/drm/apple/iomfb_internal.h +++ b/drivers/gpu/drm/apple/iomfb_internal.h @@ -112,3 +112,8 @@ void dcp_ack(struct apple_dcp *dcp, enum dcp_context_id context); /* The user may own drm_display_mode, so we need to search for our copy */ bool lookup_mode(struct apple_dcp *dcp, const struct drm_display_mode *mode, struct dcp_display_mode *out); + +int iomfb_ext26_blank(struct apple_dcp *dcp); +bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp, struct drm_framebuffer *fb); +int iomfb_ext26_retain_epoch(struct apple_dcp *dcp); +bool iomfb_ext26_control_complete(struct apple_dcp *dcp, u32 id, bool aborted); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 78162bdb805dba..5d58df80d2ac56 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -72,6 +72,18 @@ static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, { const void *data = request; + if (dcp_is_ext26(dcp)) { + if (ext26_encode(dcp, request)) { + WRITE_ONCE(dcp->crashed, true); + if (cb) + cb(dcp, NULL, cookie); + return; + } + dcp_push(dcp, oob, &dcp_methods[dcpep_swap_submit], 0x1bd8, 12, + dcp->ext26_present, cb, cookie); + return; + } + if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { iomfb_serialize_present_h17p(&dcp->present_h17p, request); if (!iomfb_apply_backlight_h17p(dcp, request, &dcp->present_h17p)) { @@ -249,8 +261,22 @@ static u32 dcpep_cb_zero(struct apple_dcp *dcp) static void dcpep_cb_swap_complete(struct apple_dcp *dcp, struct DCP_FW_NAME(dc_swap_complete_resp) *resp) { - ktime_t now = ktime_get(); + ktime_t now = dcp_is_ext26(dcp) ? ns_to_ktime(dcp->ext26_rx_ns) : ktime_get(); bool retire = true; + if (dcp_is_ext26(dcp)) { + if (iomfb_ext26_control_complete(dcp, resp->swap_id, false)) + return; + if (resp->swap_id == dcp->last_swap_id) + return; + if (dcp->present_state_h17p.pending && !dcp->present_state_h17p.accepted && + resp->swap_id == dcp->present_state_h17p.swap_id) { + dcp->ext26_early_complete = true; + dcp->ext26_early_id = resp->swap_id; + dcp->ext26_early_ns = dcp->ext26_rx_ns; + return; + } + } + #if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && @@ -702,15 +728,27 @@ dcpep_cb_allocate_buffer(struct apple_dcp *dcp, return resp; } - allocated.buf = dma_alloc_coherent(dcp->dev, size, &allocated.dva, - GFP_KERNEL); + if (dcp_is_ext26(dcp)) { + allocated.size = size; + allocated.buf = dma_alloc_attrs(dcp->dev, size, &allocated.dva, + GFP_KERNEL, DMA_ATTR_FORCE_CONTIGUOUS); + } else { + allocated.buf = dma_alloc_coherent(dcp->dev, size, &allocated.dva, GFP_KERNEL); + } if (!allocated.buf) return resp; ret = dma_get_sgtable(dcp->dev, &allocated.map, allocated.buf, allocated.dva, size); - if (ret) { - dma_free_coherent(dcp->dev, size, allocated.buf, allocated.dva); + if (ret || (dcp_is_ext26(dcp) && (allocated.map.orig_nents != 1 || + ((sg_phys(allocated.map.sgl) | allocated.dva) & (SZ_16K - 1))))) { + if (!ret) + sg_free_table(&allocated.map); + if (dcp_is_ext26(dcp)) + dma_free_attrs(dcp->dev, size, allocated.buf, allocated.dva, + DMA_ATTR_FORCE_CONTIGUOUS); + else + dma_free_coherent(dcp->dev, size, allocated.buf, allocated.dva); return resp; } @@ -967,6 +1005,10 @@ static void iomfbep_cb_enable_backlight_message_ap_gated(struct apple_dcp *dcp, /* Chunked data transfer for property dictionaries */ static u8 dcpep_cb_prop_start(struct apple_dcp *dcp, u32 *length) { +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) + return ext26_prop_start(dcp, *length); +#endif if (dcp->chunks.data != NULL) { dev_warn(dcp->dev, "ignoring spurious transfer start\n"); return false; @@ -1039,8 +1081,11 @@ static u8 dcpep_cb_prop_chunk(struct apple_dcp *dcp, return false; } - if (req->offset != dcp->chunks.received) + if (req->offset != dcp->chunks.received) { dcp->chunks.contiguous = false; + if (dcp_is_ext26(dcp)) + return false; + } if (dcp->chunks.contiguous) dcp->chunks.received += req->length; memcpy(dcp->chunks.data + req->offset, req->data, req->length); @@ -1064,6 +1109,9 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp_property_complete(dcp->chunks.data, dcp->chunks.received, dcp->chunks.length); + if (dcp_is_ext26(dcp) && (!dcp->chunks.contiguous || + dcp->chunks.received != dcp->chunks.length)) + return false; ret = parse(dcp->chunks.data, dcp->chunks.length, &ctx); if (ret) { @@ -1595,6 +1643,14 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) #endif } +static void dcp_present_rejected(struct apple_dcp *dcp) +{ + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + } +} + static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) { struct DCP_FW_NAME(dcp_swap_submit_resp) *resp = data; @@ -1608,6 +1664,8 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) status = ~0U; if (status) { + dcp_present_rejected(dcp); + dcp->ext26_early_complete = false; /* An aborted present has already been reported. */ if (ret != -ECANCELED) dev_err(dcp->dev, "swap_clear failed! status %u\n", status); @@ -1659,7 +1717,8 @@ static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, if (info) info->swap_id = resp->swap_id; - dcp_h17p_prepare_swap(dcp); + if (!dcp_is_ext26(dcp)) + dcp_h17p_prepare_swap(dcp); dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swap_cleared, cookie); } @@ -1709,6 +1768,11 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) int ret; u32 handle; dev_info(dcp->dev, "dcp_poweron() starting\n"); +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) + return; /* Native26 publishes A473 only after a successful A413. */ +#endif + #if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) @@ -2185,6 +2249,8 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) status = ~0U; if (status) { + dcp_present_rejected(dcp); + dcp->ext26_early_complete = false; /* An aborted present has already been reported. */ if (ret != -ECANCELED) dev_err(dcp->dev, "swap failed! status %u\n", status); @@ -2195,6 +2261,17 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) } return; } + if (dcp_is_ext26(dcp) && dcp->ext26_early_complete && dcp->ext26_early_id == swap_id) { + struct DCP_FW_NAME(dc_swap_complete_resp) completed = { .swap_id = swap_id }; + + u64 received = dcp->ext26_rx_ns; + + dcp->ext26_early_complete = false; + dcp->ext26_rx_ns = dcp->ext26_early_ns; + dcpep_cb_swap_complete(dcp, &completed); + dcp->ext26_rx_ns = received; + return; + } dcp->swap_start = ktime_get(); dcp->swap_submit_timestamp = arch_timer_read_counter(); dcp_swap_watchdog_arm(dcp); @@ -2204,13 +2281,15 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) { struct DCP_FW_NAME(dcp_swap_start_resp) *resp = data; - if (!resp || resp->ret) { + if (!resp || resp->ret || (dcp_is_ext26(dcp) && !resp->swap_id)) { + dcp_present_rejected(dcp); dev_err(dcp->dev, "swap_start was rejected\n"); if (dcp_present_retires(dcp)) dcp_drm_crtc_vblank(dcp->crtc); return; } if (!dcp_present_begin(dcp, resp->swap_id)) { + dcp_present_rejected(dcp); if (dcp_present_retires(dcp)) dcp_drm_crtc_vblank(dcp->crtc); return; @@ -2220,7 +2299,8 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) dcp_arm_retained_framebuffers(dcp, resp->swap_id); trace_iomfb_swap_submit(dcp, resp->swap_id); - dcp_h17p_prepare_swap(dcp); + if (!dcp_is_ext26(dcp)) + dcp_h17p_prepare_swap(dcp); dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swapped, NULL); } @@ -2276,7 +2356,10 @@ static void dcp_pre_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) { u32 identity = 0x10000; - dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); + if (dcp_is_ext26(dcp)) + do_swap(dcp, out, cookie); + else + dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); } #endif @@ -2325,13 +2408,47 @@ static void dcp_set_adaptive_sync(struct apple_dcp *dcp, u32 min_vrr, } int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, - struct drm_crtc_state *crtc_state) + struct drm_crtc_state *crtc_state +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + , struct drm_plane_state *primary +#endif + ) { struct dcp_display_mode selected, *mode = &selected; struct dcp_wait_cookie *cookie; struct dcp_color_mode *cmode = NULL; int ret; +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + u32 pitch = primary && primary->fb ? primary->fb->pitches[0] : dcp->ext26_stride; + + if (READ_ONCE(dcp->ext26_blocked)) + return -EIO; + if (READ_ONCE(dcp->pipe_enabled_h17p) && + (pitch != dcp->ext26_stride || + !drm_mode_equal(&dcp->ext26_mode, &crtc_state->mode))) { + /* A layout change cannot resume the old gentle-blank surface. */ + ret = dcp_ext26_layout_stop(dcp); + if (ret) + return ret; + } + dcp->ext26_stride = pitch; + if ((READ_ONCE(dcp->ext26_blanked) || READ_ONCE(dcp->ext26_normal)) && + READ_ONCE(dcp->pipe_enabled_h17p) && + dcp->ext26_generation == READ_ONCE(dcp->typec_generation) && + drm_mode_equal(&dcp->ext26_mode, &crtc_state->mode)) { + WRITE_ONCE(dcp->ext26_blanked, false); + return 0; + } + WRITE_ONCE(dcp->ext26_normal, false); + WRITE_ONCE(dcp->ext26_power_sent, false); + dcp->ext26_generation = READ_ONCE(dcp->typec_generation); + reinit_completion(&dcp->ext26_ready); + } + +#endif + if (!lookup_mode(dcp, &crtc_state->mode, mode)) { dev_err(dcp->dev, "no match for " DRM_MODE_FMT "\n", DRM_MODE_ARG(&crtc_state->mode)); @@ -2350,6 +2467,14 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, cmode = &mode->sdr; else if (mode->color_mode_id == mode->best.id) cmode = &mode->best; + if (dcp_is_ext26(dcp)) { + if (mode->sdr_rgb.score < 0 || mode->sdr_rgb.depth != 8 || + mode->sdr_rgb.format != DCP_COLOR_FORMAT_RGB || crtc_state->vrr_enabled) + return -EINVAL; + mode->color_mode_id = mode->sdr_rgb.id; + mode->vrr = false; + cmode = &mode->sdr_rgb; + } if (cmode) dev_info(dcp->dev, "set_digital_out_mode() color mode depth:%hhu format:%u " @@ -2377,8 +2502,17 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_get(&cookie->refcount); dcp->swap_submit_timestamp = 0; - - if (DCP_INHERIT_BOOT_MODE && +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + ret = ext26_open_client(dcp); + if (ret) { + kref_put(&cookie->refcount, release_wait_cookie); + kref_put(&cookie->refcount, release_wait_cookie); + return ret; + } + } +#endif + if (DCP_INHERIT_BOOT_MODE && !dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) { dev_dbg(dcp->dev, "inheriting the bootloader's display mode\n"); @@ -2434,6 +2568,17 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, dev_info(dcp->dev, "set_digital_out_mode returned %#x\n", READ_ONCE(cookie->status)); } +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + ret = READ_ONCE(cookie->replied) && !READ_ONCE(cookie->status) ? 0 : -EREMOTEIO; + kref_put(&cookie->refcount, release_wait_cookie); + if (!ret) + ret = ext26_activate(dcp); + if (!ret) + drm_mode_copy(&dcp->ext26_mode, &crtc_state->mode); + return ret; + } +#endif kref_put(&cookie->refcount, release_wait_cookie); dcp->vrr_enabled = mode->vrr && crtc_state->vrr_enabled; diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index 68021cff617618..227992942a968a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -302,9 +302,14 @@ struct DCP_FW_NAME(dcp_map_reg_resp) { struct apple_dcp; +struct drm_plane_state; int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, - struct drm_crtc_state *crtc_state); + struct drm_crtc_state *crtc_state +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + , struct drm_plane_state *primary +#endif + ); void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_atomic_commit *commit); void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp); void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/plane.c b/drivers/gpu/drm/apple/plane.c index cde215f62b1436..6d3dfd0f8bb9fe 100644 --- a/drivers/gpu/drm/apple/plane.c +++ b/drivers/gpu/drm/apple/plane.c @@ -5,6 +5,7 @@ #include "plane.h" +#include "dcp.h" #include "dcp-internal.h" #include "iomfb_internal.h" @@ -230,6 +231,27 @@ static int apple_plane_atomic_check(struct drm_plane *plane, if (IS_ERR(crtc_state)) return PTR_ERR(crtc_state); + { + struct platform_device *pdev = to_apple_crtc(new_plane_state->crtc)->dcp; + struct apple_dcp *target = pdev ? platform_get_drvdata(pdev) : NULL; + struct drm_framebuffer *fb = new_plane_state->fb; + + if (target && dcp_is_ext26(target) && fb && !iomfb_ext26_backing_fresh(target, fb)) + return -EINVAL; + if (target && dcp_is_ext26(target) && fb && + (plane->type != DRM_PLANE_TYPE_PRIMARY || fb->format->format != DRM_FORMAT_XRGB8888 || + fb->modifier != DRM_FORMAT_MOD_LINEAR || fb->offsets[0] || + crtc_state->vrr_enabled || new_plane_state->rotation != DRM_MODE_ROTATE_0 || + new_plane_state->src_x || new_plane_state->src_y || + new_plane_state->src_w != ((u32)crtc_state->mode.hdisplay << 16) || + new_plane_state->src_h != ((u32)crtc_state->mode.vdisplay << 16) || + new_plane_state->crtc_x || new_plane_state->crtc_y || + new_plane_state->crtc_w != crtc_state->mode.hdisplay || + new_plane_state->crtc_h != crtc_state->mode.vdisplay || + fb->width != crtc_state->mode.hdisplay || fb->height != crtc_state->mode.vdisplay)) + return -EINVAL; + } + /* * DCP limits downscaling to 2x and upscaling to 4x. Attempting to * scale outside these bounds errors out when swapping. @@ -737,15 +759,19 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange) + bool interchange, bool native26) { + static const u32 native_formats[] = { DRM_FORMAT_XRGB8888 }; struct apple_plane *plane; const u32 *fmts; u32 num_fmts; switch (type) { case DRM_PLANE_TYPE_PRIMARY: - if (supports_l10r && supports_xrgb2101010) { + if (native26) { + fmts = native_formats; + num_fmts = ARRAY_SIZE(native_formats); + } else if (supports_l10r && supports_xrgb2101010) { fmts = dcp_primary_formats; num_fmts = ARRAY_SIZE(dcp_primary_formats); } else if (supports_l10r) { diff --git a/drivers/gpu/drm/apple/plane.h b/drivers/gpu/drm/apple/plane.h index d7905573276f42..1236ca25892927 100644 --- a/drivers/gpu/drm/apple/plane.h +++ b/drivers/gpu/drm/apple/plane.h @@ -38,7 +38,7 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange); + bool interchange, bool native26); struct drm_plane *apple_plane_init_v14_7(struct drm_device *dev, unsigned long possible_crtcs, u32 iomfb_surf, diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index 6a4eefd617890f..f8ea301ab1ec05 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -138,7 +138,11 @@ int systemep_init(struct apple_dcp *dcp) init_completion(&dcp->systemep_done); dcp->systemep = afk_init(dcp, SYSTEM_ENDPOINT, systemep_ops); - afk_start(dcp->systemep); + if (IS_ERR(dcp->systemep)) + return PTR_ERR(dcp->systemep); + int ret = afk_start(dcp->systemep); + if (ret || dcp_is_ext26(dcp)) + return ret; if (!enable_verbose_logging) return 0; From 211c9439c4cff681859f11ac3e9614b6e65938c9 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:34:31 -0500 Subject: [PATCH 13/15] drm/apple: test native26 wire, attachment and PM boundaries Keep executable host fixtures for production wire encoding, admission, changesets, startup, modeset and lifecycle behavior, including classic peers. Extend the existing apple-dcp selftest harness and host-test registry; these tests do not access hardware. Retain the imported GPU UAPI fixture registration correction. Drop diagnostic-only catalogue tests. Signed-off-by: Satya Benson --- tools/asahi/dcpext26-admission-tests.py | 44 +++ tools/asahi/dcpext26-changeset-tests.py | 61 ++++ tools/asahi/dcpext26-edid-tests.py | 63 ++++ tools/asahi/dcpext26-lifecycle-tests.py | 183 +++++++++++ tools/asahi/dcpext26-modeset-tests.py | 114 +++++++ tools/asahi/dcpext26-parity-tests.py | 250 +++++++++++++++ tools/asahi/dcpext26-present-tests.py | 72 +++++ tools/asahi/dcpext26-profile-tests.py | 135 ++++++++ tools/asahi/dcpext26-startup-tests.py | 298 ++++++++++++++++++ tools/asahi/dcpext26-wire-tests.py | 149 +++++++++ tools/asahi/pmp-native26-tests.py | 106 +++++++ tools/asahi/t6030-pmp-graph-tests.c | 10 + tools/asahi/t6030-pmp-graph-tests.py | 2 +- tools/aurora-ci/host-tests.json | 98 +++++- .../selftests/drivers/gpu/apple-dcp/common.py | 58 ++++ .../selftests/drivers/gpu/apple-dcp/edid.py | 4 +- .../drivers/gpu/apple-dcp/lifecycle.py | 3 +- .../drivers/gpu/apple-dcp/recovery.py | 2 + 18 files changed, 1647 insertions(+), 5 deletions(-) create mode 100644 tools/asahi/dcpext26-admission-tests.py create mode 100644 tools/asahi/dcpext26-changeset-tests.py create mode 100644 tools/asahi/dcpext26-edid-tests.py create mode 100644 tools/asahi/dcpext26-lifecycle-tests.py create mode 100644 tools/asahi/dcpext26-modeset-tests.py create mode 100644 tools/asahi/dcpext26-parity-tests.py create mode 100644 tools/asahi/dcpext26-present-tests.py create mode 100644 tools/asahi/dcpext26-profile-tests.py create mode 100644 tools/asahi/dcpext26-startup-tests.py create mode 100644 tools/asahi/dcpext26-wire-tests.py create mode 100644 tools/asahi/pmp-native26-tests.py diff --git a/tools/asahi/dcpext26-admission-tests.py b/tools/asahi/dcpext26-admission-tests.py new file mode 100644 index 00000000000000..bd862bab759896 --- /dev/null +++ b/tools/asahi/dcpext26-admission-tests.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Actual native identity and live-session predicates; classic stays separate.""" +from pathlib import Path +import os,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +header=(ROOT/'drivers/gpu/drm/apple/j613-25g83.h').read_text() +def function(name): + pos=header.index('static inline bool '+name+'(') + return header[pos:header.index('\n}',pos)+2] +source=r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U<<(n)) +#define J613_25G83_DCP_UUID "C042E95C-B9D8-3F0E-94B3-582A08AA6FDD" +'''+function('j613_25g83_identity')+function('j613_25g83_external_state')+r''' +int main(void) { + u32 version[]={26,6,2},state[]={1,16,34,1}; + assert(j613_25g83_identity(true,1,version,3,J613_25G83_DCP_UUID)); + for(unsigned i=0;i<3;i++) { version[i]++;assert(!j613_25g83_identity(true,1,version,3,J613_25G83_DCP_UUID));version[i]--; } + assert(!j613_25g83_identity(false,1,version,3,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,0,version,3,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,1,version,2,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,1,version,3,"other image")); + assert(j613_25g83_external_state(state,4)); + for(unsigned i=0;i<4;i++)assert(!j613_25g83_external_state(state,i)); + assert(!j613_25g83_external_state(NULL,4)); + for(unsigned bit=0;bit<32;bit++) { + state[1]=16^(1U< +#include +#include +#include +#include +#include +#include +#define __init +#define GFP_KERNEL 0 +#define kzalloc_obj(x) zalloc(sizeof(x)) +#define raw_spin_lock_irqsave(l,f) ((f)=0) +#define raw_spin_unlock_irqrestore(l,f) ((void)(f)) +#define of_prop_cmp strcmp +#define __of_remove_property_sysfs(n,p) ((void)0) +#define __of_update_property_sysfs(n,p,o) ((void)0) +struct property { char *name; void *value; unsigned length; struct property *next; }; +struct device_node { struct property *properties,*deadprops; }; +struct of_changeset {int unused;};static struct of_changeset external_gate; +enum {OF_RECONFIG_ATTACH_NODE,OF_RECONFIG_DETACH_NODE,OF_RECONFIG_ADD_PROPERTY,OF_RECONFIG_REMOVE_PROPERTY,OF_RECONFIG_UPDATE_PROPERTY}; +struct of_changeset_entry {int action;struct device_node *np;struct property *prop,*old_prop;}; +static struct of_changeset_entry queued;static int queue_error,alloc_fail,alloc_calls,allocations; +static void *allocate(size_t n) { if (++alloc_calls==alloc_fail)return NULL;void *p=malloc(n);assert(p);allocations++;return p; } +static void *zalloc(size_t n){void *p=allocate(n);if(p)memset(p,0,n);return p;} +static char *kstrdup(const char *s,int f){(void)f;char *p=allocate(strlen(s)+1);if(p)strcpy(p,s);return p;} +static void *kmemdup(const void *s,size_t n,int f){(void)f;void *p=allocate(n);if(p)memcpy(p,s,n);return p;} +static void kfree(void *p){if(p){allocations--;free(p);}} +static int of_changeset_update_property(struct of_changeset *c,struct device_node *n,struct property *p){(void)c;if(queue_error)return queue_error;queued=(struct of_changeset_entry){OF_RECONFIG_UPDATE_PROPERTY,n,p,NULL};return 0;} +''' +code=pre+'\n'.join([fn(base,'__of_remove_property_from_list'),fn(base,'__of_update_property'),fn(base,'__of_remove_property'),fn(dyn,'__of_changeset_entry_invert'),fn(a,'j613_external_update')])+r''' +static void apply(void){assert(queued.action==OF_RECONFIG_UPDATE_PROPERTY);assert(!__of_update_property(queued.np,queued.prop,&queued.old_prop));} +static void revert(void){struct of_changeset_entry e;__of_changeset_entry_invert(&queued,&e);if(e.action==OF_RECONFIG_REMOVE_PROPERTY)assert(!__of_remove_property(e.np,e.prop));else{assert(e.action==OF_RECONFIG_UPDATE_PROPERTY);assert(!__of_update_property(e.np,e.prop,&e.old_prop));}} +int main(void){ + unsigned char oldbytes[4]={0,0,0,4},newbytes[4]={0,0,0,15};struct property old={.name="apple,min-state",.value=oldbytes,.length=4};struct device_node n={.properties=&old}; + assert(!j613_external_update(&n,"apple,min-state",newbytes,4));assert(n.properties==&old);apply();assert(n.properties->length==4&&!memcmp(n.properties->value,newbytes,4));revert();assert(n.properties==&old&&!memcmp(old.value,oldbytes,4)); + n=(struct device_node){0};assert(!j613_external_update(&n,"apple,min-state",newbytes,4));apply();revert();assert(!n.properties); + n=(struct device_node){0};assert(!j613_external_update(&n,"apple,always-on",NULL,0));apply();assert(n.properties&&!n.properties->length);revert();assert(!n.properties); + struct property boolean={.name="apple,always-on"};n=(struct device_node){.properties=&boolean};assert(!j613_external_update(&n,"apple,always-on",NULL,0));apply();revert();assert(n.properties==&boolean); + old.value=newbytes;n=(struct device_node){.properties=&old};assert(!j613_external_update(&n,"apple,min-state",newbytes,4));apply();revert();assert(n.properties==&old&&!memcmp(n.properties->value,newbytes,4)); + for(int i=1;i<=3;i++){alloc_calls=0;alloc_fail=i;int before=allocations;assert(j613_external_update(&n,"apple,min-state",newbytes,4)==-ENOMEM);assert(allocations==before&&n.properties==&old);} + alloc_fail=0;queue_error=-EIO;int before=allocations;assert(j613_external_update(&n,"apple,min-state",newbytes,4)==-EIO);assert(allocations==before&&n.properties==&old); + puts("PASS actual native UPDATE builder and Linux OF apply/revert: existing/absent/equal u32, existing/absent bool, allocation/queue failures"); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'test.c').write_text(code);subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-fsanitize=address,undefined',str(p/'test.c'),'-o',str(p/'test')],check=True);subprocess.run([str(p/'test')],check=True,env={**os.environ,'ASAN_OPTIONS':'detect_leaks=0'}) diff --git a/tools/asahi/dcpext26-edid-tests.py b/tools/asahi/dcpext26-edid-tests.py new file mode 100644 index 00000000000000..5244bd35f6fb9e --- /dev/null +++ b/tools/asahi/dcpext26-edid-tests.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Native AV identity/attachment predicates and public EDID wire validation.""" +from pathlib import Path +import os,re,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +source=(ROOT/'drivers/gpu/drm/apple/epic/dpavservep.c').read_text() +def function(name): + m=re.search(r'^(?:static )?bool '+name+r'\(',source,re.M);assert m + return source[m.start():source.index('\n}',m.start())+2] +pre=r''' +#include +#include +#include +#include +#include +#include "epic/dpavserv-edid.h" +typedef int64_t s64; +#define DPAVSERV_ENDPOINT 0x28 +#define DPAVSERV_ENDPOINT_V2 0x2a +#define U16_MAX 65535 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define READ_ONCE(x) (x) +#define lockdep_assert_held(x) ((void)0) +struct apple_epic_service; +struct apple_dcp { + bool native,typec_cable_connected,quiescing,external_physically_absent;u64 typec_generation;int hpd_mutex; + struct { int iomfb_method_profile; } hw; + struct { int lock;bool enabled;u64 epoch;struct apple_epic_service *service; } dcpavserv; +}; +struct ep { struct apple_dcp *dcp;u32 endpoint;bool wire_v2; }; +struct apple_epic_service { struct ep *ep;u32 channel; }; +static bool dcp_is_ext26(struct apple_dcp *d) { return d->native; } +''' +check=r''' +static void checksum(u8 *b) { u8 sum=0;b[127]=0;for(unsigned i=0;i<127;i++)sum+=b[i];b[127]=-sum; } +int main(void) { + struct apple_dcp d={.native=true,.typec_cable_connected=true,.typec_generation=7}; + struct ep ep={&d,0x28,true};struct apple_epic_service svc={&ep,1}; + assert(dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0)); + assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",1)); + assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDP13Service",0)); + ep.endpoint=0x2a;assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0)); + ep.endpoint=0x28;svc.channel=0;assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0));svc.channel=1; + d.dcpavserv.enabled=true;d.dcpavserv.service=&svc;d.dcpavserv.epoch=10; + assert(dcpavserv_edid_current(&d,7)&&dcpavserv_edid_service_current(&d,10)); + assert(!dcpavserv_edid_current(&d,6)&&!dcpavserv_edid_service_current(&d,9)); + d.external_physically_absent=true;assert(!dcpavserv_edid_current(&d,7)&&!dcpavserv_edid_service_current(&d,10)); + d.external_physically_absent=false;d.quiescing=true;assert(!dcpavserv_edid_current(&d,7)); + d.quiescing=false;d.typec_cable_connected=false;assert(!dcpavserv_edid_current(&d,7)); + u8 wire[48+8+256]={0};u8 *base=wire+56;const u8 header[]={0,255,255,255,255,255,255,0}; + afkv2_put64(wire,EDID_BUF_SIZE);afkv2_put64(wire+32,8+256);memcpy(base,header,8);base[126]=1;checksum(base);checksum(base+128); + size_t size=0;assert(!dpavserv_edid_size(wire,sizeof(wire),&size)&&size==256); + for(size_t n=0;n +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; typedef uint8_t u8; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,y) ((x)=(y)) +#define lockdep_assert_held(x) ((void)0) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define IS_ERR_OR_NULL(x) (!(x)) +#define scoped_guard(...) for (bool once=true;once;once=false) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_err(...) mock_log(__VA_ARGS__) +static void mock_log(int dev,const char *fmt,...) { (void)dev;(void)fmt; } +#define msecs_to_jiffies(x) (x) +#define DCP_SWAP_WATCHDOG_MS 2000 +#define DCP_EXTERNAL_STOP_RETRIES 3 +#define AFK_MAX_CHANNEL 3 +#define MAX_PENDING_CMDS 16 +#define spin_lock_irqsave(l,f) ((void)(l),(void)(&(f))) +#define spin_unlock_irqrestore(l,f) ((void)(l),(void)(&(f))) +static bool bitmap_empty(const unsigned long *v,unsigned n) { (void)n;return !*v; } +struct apple_epic_service { int lock; unsigned long cmd_map[1]; }; +struct ring { bool ready; u32 *rptr,*wptr; }; +struct apple_dcp_afkep { int lock,wq; struct ring rxbfr,txbfr; struct apple_epic_service services[AFK_MAX_CHANNEL]; }; +struct port { bool connected; int *service; }; +struct ext26_control; +#define SWAP_SURFACES 4 +#define guard(x) (void) +struct drm_gem_object { int tag; }; +struct drm_framebuffer { struct drm_gem_object *obj[1]; }; +struct iomfb_scanout_h17p { struct drm_framebuffer *fb[4]; struct { struct iomfb_scanout_h17p *next; } link; }; +#define list_for_each_entry(pos,head,member) for ((pos)=(head)->first;(pos);(pos)=(pos)->member.next) +struct apple_dcp { + struct { int lock; } iomfb; struct { struct iomfb_scanout_h17p *first; } ext26_retained; + bool external_suspended,ext26_pm_preparing,ext26_pm_work_off;u8 *coproc_reg; + u64 external_generation; int dev,external_error; bool external_stopped,external_power_attempted,external_running; + bool external_stop_deferred,pipe_enabled_h17p,typec_crtc_off,ext26_normal,ext26_stopping; + unsigned external_stop_retries; int external_lock,hpd_mutex,external_stop_retry_wq,rtk,avep; + struct port dptxport[1]; + struct apple_dcp_afkep *ext26_aux[3]; + struct apple_dcp_afkep *systemep,*ibootep,*dptx_controller_ep,*dptxep,*dcpavservep; + struct ext26_control *ext26_control; u32 ext26_control_id; bool ext26_control_pending,crashed,ext26_blocked; +}; +#define APPLE_DCP_COPROC_CPU_CONTROL 0x44 +#define APPLE_DCP_COPROC_CPU_CONTROL_RUN 16 +static u32 readl(const void *p) { u32 v;memcpy(&v,p,4);return v; } +static unsigned discoveries; +static void dcp_enable_typec_work(struct apple_dcp *d) { (void)d;discoveries++; } +static bool dcp_is_ext26(struct apple_dcp *d) { (void)d;return true; } +static char trace[128]; static unsigned used; static char fault; static unsigned settlements; +static void note(char c) { assert(usedservice; } +static int dptxport_set_hpd(int *s,bool on) { assert(s&&!on);return result('H'); } +static int dptxport_release_display(int *s) { assert(s);return result('R'); } +static void apple_rtkit_flush_rx(int r) { (void)r;note('X'); } +static void flush_workqueue(int q) { (void)q;note('W'); } +static int iomfb_ext26_retain_epoch(struct apple_dcp *d) { (void)d;return result('K'); } +static void dpavservep_detach(struct apple_dcp *d) { (void)d;note('E'); } +static void dcp_modes_begin_attachment(struct apple_dcp *d) { (void)d;note('M'); } +static void av_service_disconnect(struct apple_dcp *d) { (void)d;note('A'); } +static int system_dfl_wq; +static void mod_delayed_work(int q,int *w,int t) { (void)q;(void)w;(void)t;note('D'); } +struct completion { unsigned signaled; }; +static void complete_all(struct completion *c) { c->signaled++; } +#define DCPEXT26_SUBMIT_BYTES 0x1bd8 +struct ext26_control { struct dcpext26_exchange exchange; struct completion done; u8 wire[DCPEXT26_SUBMIT_BYTES]; }; +''' +checks = r''' +static struct apple_dcp fresh(struct apple_dcp_afkep *ep) { + static int service; struct apple_dcp d={0}; d.external_generation=7; d.external_power_attempted=true; + d.external_running=true; d.pipe_enabled_h17p=true; d.ext26_normal=true; + d.dptxport[0].connected=true; d.dptxport[0].service=&service; d.systemep=ep; + used=settlements=0;trace[0]=0;fault=0;return d; +} +static void native_pm_complete(struct apple_dcp *dcp); +static int native_pm_resume(struct apple_dcp *dcp); +int main(void) { + struct ext26_control c={0}; struct apple_dcp d={0};d.ext26_control=&c;d.ext26_control_pending=true; + assert(!dcpext26_exchange_begin(&c.exchange,0)); + assert(dcpext26_exchange_begin(&c.exchange,42));assert(!dcpext26_exchange_begin(&c.exchange,43)); + assert(!iomfb_ext26_control_complete(&d,41,false));assert(!c.done.signaled); + assert(iomfb_ext26_control_complete(&d,42,false));assert(!c.done.signaled&&d.ext26_control_pending); + c.exchange.replied=true; ext26_control_signal(&d,&c);assert(c.done.signaled&&!d.ext26_control_pending); + c=(struct ext26_control){0};d.ext26_control=&c;d.ext26_control_pending=true; + assert(dcpext26_exchange_begin(&c.exchange,51));c.exchange.replied=true; + ext26_control_signal(&d,&c);assert(!c.done.signaled); + assert(iomfb_ext26_control_complete(&d,51,true));assert(c.exchange.failed&&c.done.signaled); + assert(!dcpext26_epoch_fits(8,1,1)); assert(dcpext26_epoch_fits(7,0,(960ULL<<20))); + assert(!dcpext26_epoch_fits(7,(960ULL<<20),1));assert(!dcpext26_epoch_fits(0,~0ULL,1)); + struct apple_dcp_afkep ep={0};u32 read=0,write=0; + ep.rxbfr=(struct ring){true,&read,&write};assert(afk_host_idle(&ep)); + write=64;assert(!afk_host_idle(&ep));read=64;assert(afk_host_idle(&ep)); + ep.services[1].cmd_map[0]=1;assert(!afk_host_idle(&ep));ep.services[1].cmd_map[0]=0; + d=fresh(&ep);assert(!dcp_external_stop_locked(&d,6)&&!used); /* stale stop token */ + assert(!dcp_external_stop_locked(&d,7));assert(!strcmp(trace,"FEQPQHRXWXQKM")); + assert(d.external_stopped&&!d.pipe_enabled_h17p&&!d.ext26_normal&&!d.ext26_stopping); + assert(d.typec_crtc_off&&!d.dptxport[0].connected); + assert(!dcp_external_stop_locked(&d,7)); /* idempotent stop */ + d=fresh(&ep);fault='P';assert(dcp_external_stop_locked(&d,7)==-EIO); + assert(d.external_error==-EIO&&!d.external_stopped&&d.dptxport[0].connected); + assert(!strcmp(trace,"FEQP"));assert(dcp_external_stop_locked(&d,7)==-EIO); + d=fresh(&ep);fault='H';assert(dcp_external_stop_locked(&d,7)==-EIO);assert(!strchr(trace,'R')); + d=fresh(&ep);fault='R';assert(dcp_external_stop_locked(&d,7)==-EIO);assert(!strchr(trace,'K')); + d=fresh(&ep);ep.services[0].cmd_map[0]=1;assert(dcp_external_stop_locked(&d,7)==-EBUSY); + assert(!strchr(trace,'K'));ep.services[0].cmd_map[0]=0; + d=fresh(&ep);d.external_power_attempted=false;assert(!dcp_external_stop_locked(&d,7)); + assert(!strchr(trace,'P')&&strchr(trace,'K')); /* no-sink / pre-picture stop */ + d=fresh(&ep);assert(d.external_running);assert(!dcp_external_start_locked(&d));assert(!strcmp(trace,"LU")); + d=fresh(&ep);d.external_running=false;assert(!dcp_external_start_locked(&d));assert(d.external_running&&!strcmp(trace,"LU")); + struct drm_gem_object oldobj={1},newobj={2}; + struct drm_framebuffer oldfb={{&oldobj}},aliasfb={{&oldobj}},newfb={{&newobj}}; + struct iomfb_scanout_h17p retained={.fb={&oldfb}};d.ext26_retained.first=&retained; + assert(!iomfb_ext26_backing_fresh(&d,&oldfb));assert(!iomfb_ext26_backing_fresh(&d,&aliasfb)); + assert(iomfb_ext26_backing_fresh(&d,&newfb)); + u8 registers[0x50]={0};u32 running=16;memcpy(registers+0x44,&running,4); + d=(struct apple_dcp){.coproc_reg=registers,.ext26_pm_preparing=true,.ext26_pm_work_off=true,.external_suspended=true}; + assert(!native_pm_resume(&d));assert(!discoveries&&d.ext26_pm_preparing&&d.ext26_pm_work_off); + native_pm_complete(&d);assert(discoveries==1&&!d.ext26_pm_preparing&&!d.ext26_pm_work_off); + native_pm_complete(&d);assert(discoveries==1); + memset(registers,0,sizeof(registers));d.ext26_pm_preparing=d.ext26_pm_work_off=true; + assert(native_pm_resume(&d)==-EHOSTDOWN&&d.ext26_blocked&&d.external_error==-EHOSTDOWN); + native_pm_complete(&d);assert(discoveries==1); /* a dead coprocessor gets no discovery */ + puts("PASS native26 exact control order, stale IDs, abort, retained-stop sequencing/faults, host drain, old-GEM rejection, epoch bounds and PM-complete ordering"); +} +''' +life = (ROOT/'drivers/gpu/drm/apple/dcpext26-lifecycle.h').read_text().replace('#include ','') +# Put the pure state before the mock control container. +pre = pre.replace('struct ext26_control { struct dcpext26_exchange',life+'\nstruct ext26_control { struct dcpext26_exchange') +code = pre + '\n' + '\n'.join([ + body('drivers/gpu/drm/apple/afk.c','afk_host_idle'), + body('drivers/gpu/drm/apple/iomfb.c','iomfb_ext26_backing_fresh'), + body('drivers/gpu/drm/apple/iomfb_h17p.c','ext26_control_signal'), + body('drivers/gpu/drm/apple/iomfb_h17p.c','iomfb_ext26_control_complete'), + body('drivers/gpu/drm/apple/dcp.c','dcp_external_stop_locked')]) + checks +# dcp.c has a forward declaration; select the actual definition. +s=(ROOT/'drivers/gpu/drm/apple/dcp.c').read_text();pos=s.index('static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation)\n{');actual=s[pos:s.index('\n}',pos)+2] +code=code[:code.index('static int dcp_external_stop_locked')] + actual + '\n' + body('drivers/gpu/drm/apple/dcp.c','dcp_external_start_locked') + checks +# Execute the native branches of the actual PM hooks, preserving their returns. +def native_branch(name): + pos=s.index('static '+('void' if name.endswith('complete') else 'int')+' '+name+'(') + pos=s.index('if (dcp_is_ext26(dcp)) {',pos) + opening=s.index('{',pos);end=opening+1;depth=1 + while depth: + depth+=(s[end]=='{')-(s[end]=='}');end+=1 + return s[pos:end] +code += '\nstatic void native_pm_complete(struct apple_dcp *dcp) { '+native_branch('dcp_platform_complete')+' }\n' +code += 'static int native_pm_resume(struct apple_dcp *dcp) { int dev=0; '+native_branch('dcp_platform_resume')+' return -EINVAL; }\n' + +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'check.c').write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-fsanitize=address,undefined',str(p/'check.c'),'-o',str(p/'check')],check=True) + subprocess.run([str(p/'check')],check=True) diff --git a/tools/asahi/dcpext26-modeset-tests.py b/tools/asahi/dcpext26-modeset-tests.py new file mode 100644 index 00000000000000..0c3afa1b0638dc --- /dev/null +++ b/tools/asahi/dcpext26-modeset-tests.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise native preflight with DRM's detached CRTC state after state swap.""" +from pathlib import Path +import subprocess,tempfile +R=Path(__file__).resolve().parents[2];s=(R/'drivers/gpu/drm/apple/iomfb_template.c').read_text();caller=(R/'drivers/gpu/drm/apple/iomfb.c').read_text() +a=s.index('\tif (dcp_is_ext26(dcp)) {',s.index('int DCP_FW_NAME(iomfb_modeset)')) +b=s.index('\n\tif (!lookup_mode',a) +a=s.rfind('#if DCP_FW_VER',0,a) +body=s[a:b] +assert 'crtc_state->state' not in body +assert 'drm_atomic_get_new_plane_state(commit, crtc->primary)' in caller +pre=r''' +#include +#include +#include +#include +typedef uint32_t u32; +#define DCP_FW_VERSION(a,b,c) ((a)*10000+(b)*100+(c)) +#define DCP_FW_VER DCP_FW_VERSION(26,6,0) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +struct mode {int id;}; +struct drm_framebuffer {u32 pitches[1];}; +struct drm_plane_state {struct drm_framebuffer *fb;}; +struct drm_crtc_state {void *state;struct mode mode;}; +struct apple_dcp {bool ext26_blocked,pipe_enabled_h17p,ext26_blanked,ext26_normal,ext26_power_sent;u32 ext26_stride,ext26_generation,typec_generation;struct mode ext26_mode;int ext26_ready;}; +static bool dcp_is_ext26(struct apple_dcp *d){return true;} +static bool drm_mode_equal(const struct mode *a,const struct mode *b){return a->id==b->id;} +static unsigned stops; +static int dcp_ext26_layout_stop(struct apple_dcp *d){stops++;d->pipe_enabled_h17p=false;return 0;} +#define reinit_completion(c) (*(c)=0) +''' +code=pre+'static int prepare(struct apple_dcp *dcp,struct drm_crtc_state *crtc_state,struct drm_plane_state *primary){int ret;\n'+body+'\nreturn 0;}'+r''' +int main(void){ + struct drm_crtc_state c={.state=0,.mode.id=1};struct drm_framebuffer fb={.pitches={15360}};struct drm_plane_state p={.fb=&fb};struct apple_dcp d={.typec_generation=7}; + assert(!prepare(&d,&c,&p)&&d.ext26_stride==15360&&d.ext26_generation==7); + d.pipe_enabled_h17p=true;d.ext26_blanked=true;d.ext26_mode=c.mode; + assert(!prepare(&d,&c,&p)&&!d.ext26_blanked&&!stops); + // G's post-resume repeat must preserve the already proven active mode. + d.ext26_normal=true;d.ext26_power_sent=true;d.ext26_ready=1; + assert(!prepare(&d,&c,&p)&&d.ext26_normal&&d.ext26_power_sent&&d.ext26_ready==1&&!stops); + // A different physical generation must still acquire fresh readiness. + d.typec_generation++;assert(!prepare(&d,&c,&p)&&!d.ext26_normal&&!d.ext26_power_sent&&d.ext26_ready==0); + fb.pitches[0]=16384;assert(!prepare(&d,&c,&p)&&stops==1&&d.ext26_stride==16384); + d.ext26_blocked=true;assert(prepare(&d,&c,&p)==-EIO); + d.ext26_blocked=false;assert(!prepare(&d,&c,0)); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code);subprocess.run(['clang','-std=gnu11','-Wall','-Werror','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True);subprocess.run([str(p.with_suffix(''))],check=True) +print('PASS actual native preflight: detached CRTC atomic backlink, 4K pitch, gentle resume, changed layout, missing plane and blocked session') + +# The driver enable callback must finish route ownership and wake a pending flip +# on native modeset errors; the DRM core, not this callback, owns commit completion. +drv=(R/'drivers/gpu/drm/apple/apple_drv.c').read_text() +a=drv.index('static void apple_crtc_atomic_enable(');b=drv.index('\n}',a)+2 +actual=drv[a:b] +preamble=r''' +#include +#include +#include +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +struct drm_crtc {int unused;}; +struct drm_crtc_state {bool active,active_changed,color_mgmt_changed;}; +struct drm_atomic_commit {struct drm_crtc_state *state;unsigned failed_routes;}; +struct apple_dcp {void *dev;bool native,main_display;int mode_state,vblank_wq;void *connector;}; +struct apple_crtc {struct apple_dcp *dcp;bool fabric_held;}; +static struct apple_crtc owner; +static struct apple_crtc *to_apple_crtc(struct drm_crtc *c){return &owner;} +static struct apple_dcp *platform_get_drvdata(struct apple_dcp *d){return d;} +static struct drm_crtc_state *drm_atomic_get_new_crtc_state(struct drm_atomic_commit *c,struct drm_crtc *r){return c->state;} +#define to_apple_atomic_state(c) (c) +static unsigned drm_crtc_mask(struct drm_crtc *c){return 1;} +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static unsigned done,woken,notified,invalidated,powered,programmed; +static int mode_error; +static int dcp_typec_follow_crtc(struct drm_crtc *c,struct drm_atomic_commit *a,bool *locked){*locked=true;return 0;} +static void dcp_typec_follow_done(bool locked){assert(locked);done++;} +static bool dcp_crtc_route_ready(struct drm_crtc *c,struct drm_atomic_commit *a,bool enable){return true;} +static bool dcp_crtc_needs_route_start(struct apple_dcp *d){return false;} +static int dcp_typec_prepare_route(struct apple_dcp *d){return 0;} +static int apple_drm_fabric_hold(void){return 0;} +static void dcp_poweron(struct apple_dcp *d){powered++;} +static void dcp_external_recover(struct apple_dcp *d){} +static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s){return true;} +static int dcp_crtc_atomic_modeset(struct drm_crtc *c,struct drm_atomic_commit *a){programmed++;return mode_error;} +static void dcp_mode_invalidate(int *m){invalidated++;*m=0;} +static void schedule_work(int *w){woken++;} +static void dcp_route_failure_notify(void *c){notified++;} +''' +checks=r''' +int main(void){ + struct drm_crtc crtc={0};struct drm_crtc_state state={.active=true,.active_changed=true}; + struct drm_atomic_commit commit={.state=&state};struct apple_dcp d={.native=true,.connector=(void *)1};owner.dcp=&d; + const int errors[]={-EIO,-ENOMEM,-ETIMEDOUT,-EPIPE}; + for(unsigned i=0;i<4;i++){ + done=woken=notified=invalidated=powered=programmed=0;mode_error=errors[i];d.mode_state=1; + apple_crtc_atomic_enable(&crtc,&commit); + assert(done==1&&woken==1&¬ified==1&&invalidated==1&&programmed==1&&powered==1&&!d.mode_state); + } + done=woken=notified=invalidated=0;mode_error=0; + apple_crtc_atomic_enable(&crtc,&commit);assert(done==1&&!woken&&!notified&&!invalidated); + // Existing non-native error policy is preserved. + d.native=false;done=woken=notified=invalidated=0;mode_error=-EIO; + apple_crtc_atomic_enable(&crtc,&commit);assert(done==1&&!woken&&!notified&&!invalidated); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t)/'enable.c';p.write_text(preamble+actual+checks) + subprocess.run(['clang','-std=gnu11','-Wall','-Werror','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + subprocess.run([str(p.with_suffix(''))],check=True) +print('PASS actual enable callback: native mode errors invalidate/wake flips/finish route once; success and classic policy preserved') diff --git a/tools/asahi/dcpext26-parity-tests.py b/tools/asahi/dcpext26-parity-tests.py new file mode 100644 index 00000000000000..4cb08bba288edf --- /dev/null +++ b/tools/asahi/dcpext26-parity-tests.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Ordered native startup and the native DP callback contract, actual C bodies. + +Synthetic transport/MMIO boundaries. This cannot prove a physical picture or PM. +""" +from pathlib import Path +import os,re,subprocess,tempfile +R=Path(__file__).resolve().parents[2] +def fn(s,n): + m=re.search(r'^(?:static )?(?:int|bool|void) '+n+r'\([^;]*?\)\n\{',s,re.M|re.S);assert m,n + i=m.end();depth=1 + while depth:depth+=(s[i]=='{')-(s[i]=='}');i+=1 + return s[m.start():i] +def run(name,code): + with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-Wno-missing-field-initializers','-Wno-sign-compare','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + subprocess.run([str(p.with_suffix(''))],check=True) + print('PASS',name) +dcp=(R/'drivers/gpu/drm/apple/dcp.c').read_text() +pre=r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8;typedef uint16_t u16;typedef uint32_t u32;typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,y) ((x)=(y)) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define IS_ERR(p) ((intptr_t)(p)<0) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define ERR_PTR(e) ((void *)(intptr_t)(e)) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define APPLE_DCP_COPROC_CPU_CONTROL 0x44 +#define APPLE_DCP_COPROC_CPU_CONTROL_RUN 16 +#define APPLE_DCP_COPROC_CPU_STATUS 0x48 +#define msecs_to_jiffies(x) (x) +struct device {int unused;}; +struct apple_dcp_afkep {u8 endpoint;bool match_epic_name;}; +struct apple_dcp {struct device *dev;u8 *coproc_reg;void *rtk;bool crashed;int ext26_link_ready; + struct apple_dcp_afkep *ext26_aux[3];}; +static char trace[256];static unsigned used;static char fault; +static void note(char c){assert(used<255);trace[used++]=c;trace[used]=0;} +static int step(char c){note(c);return fault==c?-EIO:0;} +static void reset(char f){memset(trace,0,sizeof(trace));used=0;fault=f;} +static int dev_err_probe(struct device *d,int e,const char *fmt,...){return e;} +static u32 readl(void *p){u32 x;memcpy(&x,p,4);return x;} +static void writel(u32 x,void *p){assert(x&16);note('R');memcpy(p,&x,4);} +static int dcp_pin_live_session(struct apple_dcp *d){return step('P');} +static const int rtkit_ops; +static void *apple_rtkit_init(struct device *dev,struct apple_dcp *d,const char *name,int channel,const int *ops){assert(!strcmp(name,"mbox")&&!channel&&ops==&rtkit_ops);return step('I')?ERR_PTR(-EIO):d;} +static void dcp_release_rtkit(void *d){} +static int devm_add_action_or_reset(struct device *d,void (*action)(void *),void *p){return step('A');} +static int apple_rtkit_wake(void *r){return step('W');} +static int apple_rtkit_boot(void *r){return step('B');} +static int iomfb_start_rtkit(struct apple_dcp *d){return step('L');} +static unsigned wait_for_completion_timeout(int *c,unsigned t){note('l');return fault!='l';} +static int systemep_init(struct apple_dcp *d){return step('0');} +static int ibootep_init(struct apple_dcp *d){return step('3');} +static int dptx_controller_init(struct apple_dcp *d){return step('4');} +static int dpavservep_init(struct apple_dcp *d){return step('8');} +static int dptxep_init(struct apple_dcp *d){return step('a');} +static const int ext26_aux_ops[1]; +static struct apple_dcp_afkep aux[3];static unsigned auxiliary; +static struct apple_dcp_afkep *afk_init(struct apple_dcp *d,u8 ep,const int *ops){assert(ops==ext26_aux_ops);struct apple_dcp_afkep *a=&aux[auxiliary++%3];a->endpoint=ep;return a;} +static int afk_start(struct apple_dcp_afkep *ep){char c=ep->endpoint==0x27?'7':ep->endpoint==0x29?'9':'c';return step(c);} +static void iomfb_ext26_start(struct apple_dcp *d){note('S');} +''' +checks=r''' +int main(void){ + u8 mmio[0x50]={0};struct device dev={0};struct apple_dcp d={.dev=&dev,.coproc_reg=mmio};u32 run=16; + reset(0);memcpy(mmio+0x44,&run,4);assert(!dcp_ext26_start_resident(&d));assert(!strcmp(trace,"PIAW")); + reset(0);memset(mmio,0,sizeof(mmio));assert(!dcp_ext26_start_resident(&d));assert(!strcmp(trace,"PIARB")&&readl(mmio+0x44)==16); + const char stages[]="PIAWB"; + for(unsigned i=0;idp.lanes=capacity;return fault;} +static int dcp_direct_crossbar_link(struct apple_dcp *d,bool on){if(on)up++;else down++;return fault;} +static void mdelay(unsigned n){assert(n==10);delay++;} +static void complete(int *c){(*c)++;} +static int dptx_j700_training(struct apple_epic_service *s,bool w,const void *d,size_t n,void *r,size_t room){training++;return 0;} +static int dptx_j700_validate_lanes(struct dptx_port *p,u64 n){return n<=capacity&&n!=3?0:-EINVAL;} +static int dptxport_call_set_link_rate(struct apple_epic_service *s,const void *d,size_t n,void *r,size_t room){assert(down);return 0;} +static int dptxport_call(struct apple_epic_service *s,u32 cmd,const void *d,size_t n,void *r,size_t room){fallback++;return 0;} +''' +run('native DP callback wire and ordering',pre2+fn(s,'dptxport_native26_call')+r''' +int main(void){ + struct apple_dcp d={0};struct ep ep={&d};struct dptx_port p={.atcphy=&d};struct apple_epic_service svc={&ep,&p};u8 input[160]={0},out[160]; + #define CALL(c,n,r) dptxport_native26_call(&svc,0,(c),input,(n),out,(r)) + memset(out,0xa5,sizeof(out));assert(!CALL(2,32,32));assert(word(out+20)==3&&word(out+24)==3&&!word(out+16)); + assert(!CALL(7,32,32)&&word(out+16)==0x14); + assert(!CALL(18,32,32)&&word(out+16)==1);assert(!CALL(21,160,160)&&word(out)==1); + assert(!CALL(10,32,32)&&p.native26_max_lanes==4&&!p.lane_count); + put_unaligned_le64(4,input+16);assert(!CALL(12,32,32)&&p.lane_count==4&&p.linkcfg_completion==1); + put_unaligned_le64(3,input+16);assert(CALL(12,32,32)==-EINVAL); + assert(!CALL(0,16,16)&&up==1);assert(!CALL(1,16,16)&&down==1); + assert(!CALL(5,16,16)&&down==2);p.link_rate=0x14;assert(!CALL(6,16,16)&&delay==1&&up==2); + fault=-EIO;assert(CALL(0,16,16)==-EIO);put_unaligned_le64(0x14,input+16);assert(CALL(9,32,32)==-EIO&&!fallback);fault=0; + put_unaligned_le64(0x99,input+16);assert(CALL(9,32,32)==-EINVAL); + assert(!dptxport_native26_call(&svc,1,3,input,80,out,80)&&training==1); + assert(dptxport_native26_call(&svc,2,3,input,80,out,80)==-EOPNOTSUPP); + puts("native DP HPD/tiled=1, drive offsets20/24, maxHBR2, distinct max/active lanes, crossbar errors propagated"); +} +''') +# Deferred OPEN executes only after A401, including ops without a prepare/revalidate hook. +s=(R/'drivers/gpu/drm/apple/afk.c').read_text() +pre3=pre[:pre.index('struct device {')]+r''' +#define kfree(p) ((void)0) +#define dev_err(...) ((void)0) +#define EPIC_TYPE_NOTIFY 1 +#define EPIC_CAT_REPORT 2 +#define AFKV2_OPEN 3 +struct apple_epic_service; +struct apple_epic_service_ops {int (*revalidate)(struct apple_epic_service *,const char *,const char *,int);void (*init)(struct apple_epic_service *,const char *,const char *,int);}; +struct apple_epic_service {bool pending_open,enabled;const struct apple_epic_service_ops *ops;const char *pending_name,*pending_class;int pending_unit;u32 channel;}; +struct apple_dcp {bool ext26_services_open;struct apple_dcp_afkep *dptxep;}; +static bool dptx_native_controllers_ready(struct apple_dcp *d){return true;} +struct apple_dcp_afkep {struct apple_dcp *dcp;unsigned num_channels;struct apple_epic_service services[2];}; +static bool dcp_is_ext26(struct apple_dcp *d){return true;} +static unsigned sent,published,discarded,initialized;static int fault; +static void afk_discard_pending_open(struct apple_epic_service *s){discarded++;s->pending_open=false;} +static void *afk_epic_find_service(struct apple_dcp_afkep *e,u32 c){return NULL;} +static int afk_send_epic(struct apple_dcp_afkep *e,u32 c,u16 tag,int type,int cat,u8 subtype,const void *data,size_t size){assert(c&&subtype==AFKV2_OPEN&&!size);sent++;return fault;} +static void afk_service_publish(struct apple_epic_service *s){published++;s->enabled=true;} +static void afk_populate_service_debugfs(struct apple_epic_service *s){} +static void initialized_service(struct apple_epic_service *s,const char *n,const char *c,int u){initialized++;} +''' +run('A401 service OPEN barrier',pre3+fn(s,'afk_retry_service_init')+r''' +int main(void){struct apple_dcp d={0};struct apple_epic_service_ops ops={.init=initialized_service};struct apple_dcp_afkep e={.dcp=&d,.num_channels=1};e.services[0]=(struct apple_epic_service){.pending_open=true,.ops=&ops,.channel=1}; + afk_retry_service_init(&e);assert(!sent&&!published&&!initialized&&e.services[0].pending_open); + d.ext26_services_open=true;afk_retry_service_init(&e);assert(sent==1&&published==1&&initialized==1&&!e.services[0].pending_open&&e.services[0].enabled); + afk_retry_service_init(&e);assert(sent==1);ops.init=NULL;e.services[0].pending_open=true;fault=-EIO;afk_retry_service_init(&e);assert(discarded==1&&published==1); + puts("native deferred OPEN survives absent revalidate/init hooks, opens after A401 once, rejects failed transport");} +''') +# Native property transfer's sentinel count and replacing a prior transfer. +s=(R/'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +# fn() also accepts the u8 return type for this small callback. +m=re.search(r'^static u8 ext26_prop_start\([^;]*?\)\n\{',s,re.M|re.S);a=m.start();i=m.end();depth=1 +while depth:depth+=(s[i]=='{')-(s[i]=='}');i+=1 +run('native property transfer count',pre[:pre.index('struct device {')]+r''' +#define SZ_1M (1U<<20) +#define GFP_KERNEL 0 +#define kfree free +#define max_t(t,a,b) ((t)(a)>(t)(b)?(t)(a):(t)(b)) +static bool fail_alloc; +static void *kzalloc(size_t n,int f){return fail_alloc?NULL:calloc(1,n);} +struct dcp_chunks {void *data;size_t length,received;bool contiguous;u64 modes_generation;}; +struct apple_dcp {struct dcp_chunks chunks;}; +static u64 generation; +static u64 dcp_modes_transfer_begin(struct apple_dcp *d){return ++generation;} +static void dcp_firmware_metadata_begin(struct apple_dcp *d){} +'''+s[a:i]+r''' +int main(void){struct apple_dcp d={0};assert(!ext26_prop_start(&d,0));assert(!ext26_prop_start(&d,(1U<<20)+2)); + assert(ext26_prop_start(&d,4097)&&d.chunks.length==4096&&d.chunks.contiguous&&!d.chunks.received); + memset(d.chunks.data,0x55,4096);d.chunks.received=11;assert(ext26_prop_start(&d,2)&&d.chunks.length==1&&!d.chunks.received&&!*(u8 *)d.chunks.data); + assert(ext26_prop_start(&d,1)&&d.chunks.length==0&&d.chunks.data);free(d.chunks.data);d.chunks.data=NULL; + fail_alloc=true;assert(!ext26_prop_start(&d,4097)&&!d.chunks.data);puts("native D127 count-1, replaced transfer, empty payload, bounds and allocation refusal");} +''') +# Physical-address allocation and PIODMA lifetime: exercise the common allocator's +# native branch, plus the native wire admission, rather than a model allocator. +s=(R/'drivers/gpu/drm/apple/iomfb_template.c').read_text();pos=s.index('dcpep_cb_allocate_buffer(struct');a=s.rfind('static struct ',0,pos);i=s.index('\n}',pos)+2 +alloc=s[a:i] +h=(R/'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +pre4=pre[:pre.index('struct device {')]+r''' +#define SZ_16K 16384UL +#define DCP_MAX_MAPPINGS 128 +#define GFP_KERNEL 0 +#define DMA_ATTR_FORCE_CONTIGUOUS 8 +#define DCP_FW_VERSION(a,b,c) ((a)*10000+(b)*100+(c)) +#define DCP_FW_VER DCP_FW_VERSION(26,6,0) +#define DCP_FW_NAME(n) n +#define round_down(x,a) ((x)&~((__typeof__(x))(a)-1)) +#define check_add_overflow(a,b,p) __builtin_add_overflow((a),(b),(p)) +#define dev_warn(...) ((void)0) +struct scatterlist {u64 phys;}; +struct sg_table {struct scatterlist *sgl;unsigned orig_nents;}; +struct dcp_mem_descriptor {size_t size;void *buf;u64 dva;struct sg_table map;bool piodma_mapped;}; +struct apple_dcp {bool native;void *dev;struct dcp_mem_descriptor memdesc[128];unsigned long memdesc_map[2],ext26_released[2];}; +struct dcp_allocate_buffer_req {u64 size;}; +struct dcp_allocate_buffer_resp {u32 mem_desc_id;u64 dva_size,dva,paddr;}; +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static bool test_bit(unsigned id,const unsigned long *b){return !!(b[id/64]&(1UL<<(id%64)));} +static void set_bit(unsigned id,unsigned long *b){b[id/64]|=1UL<<(id%64);} +static unsigned find_first_zero_bit(unsigned long *b,unsigned n){for(unsigned i=0;isgl=&sg;t->orig_nents=fragmented?2:1;return 0;} +static void sg_free_table(struct sg_table *t){} +static u64 sg_phys(struct scatterlist *s){return s->phys;} +static void dma_free_attrs(void *d,size_t n,void *p,u64 a,unsigned attrs){assert(attrs==8);frees++;free(p);} +static void dma_free_coherent(void *d,size_t n,void *p,u64 a){frees++;free(p);} +static u64 get_unaligned_le64(const void *p){u64 v;memcpy(&v,p,8);return v;} +static u32 get_unaligned_le32(const void *p){u32 v;memcpy(&v,p,4);return v;} +static void put_unaligned_le32(u32 v,void *p){memcpy(p,&v,4);} +static void put_unaligned_le64(u64 v,void *p){memcpy(p,&v,8);} +static bool trampoline_map_piodma_h17p(struct apple_dcp *d,int tag,void *out,void *in){u64 id=get_unaligned_le64(in);maps++;d->memdesc[id].piodma_mapped=true;put_unaligned_le32(id,out);put_unaligned_le64(d->memdesc[id].dva,(u8 *)out+4);return true;} +''' +run('native contiguous buffer and PIODMA wire ownership',pre4+alloc+fn(h,'ext26_map_piodma')+r''' +int main(void){struct apple_dcp d={.native=true};set_bit(0,d.memdesc_map);struct dcp_allocate_buffer_req q={4096}; + struct dcp_allocate_buffer_resp r=dcpep_cb_allocate_buffer(&d,&q);assert(r.mem_desc_id==1&&r.dva_size==16384&&r.paddr==0x200000&&r.dva==0x800000&&attributes==8); + u8 in[12]={0},out[16];put_unaligned_le64(1,in);put_unaligned_le32(0,in+8);ext26_map_piodma(&d,201,out,in);assert(!maps&&!get_unaligned_le32(out)); + put_unaligned_le32(1,in+8);ext26_map_piodma(&d,201,out,in);assert(maps==1&&get_unaligned_le32(out)==1&&get_unaligned_le64(out+4)==r.dva&&!get_unaligned_le32(out+12)); + ext26_map_piodma(&d,201,out,in);assert(maps==1&&get_unaligned_le64(out+4)==r.dva);set_bit(1,d.ext26_released);ext26_map_piodma(&d,201,out,in);assert(maps==1&&!get_unaligned_le32(out)&&d.memdesc[1].buf); + fragmented=true;r=dcpep_cb_allocate_buffer(&d,&q);assert(!r.mem_desc_id&&frees==1&&!test_bit(2,d.memdesc_map)); + free(d.memdesc[1].buf);puts("native D451 physically contiguous16K reply and D201 word1, packed16-byte reply, repeat mapping and logical retention");} +''') diff --git a/tools/asahi/dcpext26-present-tests.py b/tools/asahi/dcpext26-present-tests.py new file mode 100644 index 00000000000000..930c808a712b54 --- /dev/null +++ b/tools/asahi/dcpext26-present-tests.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Actual DRM primary start/submit callbacks: faults and early exact completion.""" +from pathlib import Path +import os,re,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +s=(ROOT/'drivers/gpu/drm/apple/iomfb_template.c').read_text() +def function(name): + m=re.search(r'^static void '+name+r'\(',s,re.M);assert m + return s[m.start():s.index('\n}',m.start())+2] +pre=r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32;typedef uint64_t u64; +#define WRITE_ONCE(x,y) ((x)=(y)) +#define DCP_FW_NAME(n) n +#define DCP_FW_UNION(x) ((x).h17p) +#define dev_err(...) ((void)0) +#define trace_iomfb_swap_submit(...) ((void)0) +struct dc_swap_complete_resp { u32 swap_id; }; +struct dcp_swap_submit_resp { u32 ret; }; +struct dcp_swap_start_resp { u32 swap_id,ret; }; +struct apple_dcp { bool native,crashed,ext26_blocked,ext26_early_complete;u32 ext26_early_id; + u64 ext26_rx_ns,ext26_early_ns,swap_start,swap_submit_timestamp;void *dev,*crtc; + struct { struct { struct {u32 swap_id;} swap; } h17p; } swap; }; +static int submit_result;static bool begin_allowed=true; +static unsigned failures,events,armed,watchdogs,completed,sent;static u64 completion_time; +static bool dcp_is_ext26(struct apple_dcp *d) { return d->native; } +static int dcp_present_submit(struct apple_dcp *d,u32 id,bool ok) { (void)d;(void)id;(void)ok;return submit_result; } +static void dcp_present_failed(struct apple_dcp *d) { (void)d;failures++; } +static bool dcp_present_retires(struct apple_dcp *d) { (void)d;return true; } +static void dcp_unarm_retained_framebuffers(struct apple_dcp *d,u32 id) { (void)d;(void)id; } +static void dcp_drm_crtc_vblank(void *c) { (void)c;events++; } +static void dcpep_cb_swap_complete(struct apple_dcp *d,struct dc_swap_complete_resp *r) { assert(r->swap_id==d->swap.h17p.swap.swap_id);completed++;completion_time=d->ext26_rx_ns; } +static u64 ktime_get(void) { return 30; } +static u64 arch_timer_read_counter(void) { return 40; } +static void dcp_swap_watchdog_arm(struct apple_dcp *d) { (void)d;watchdogs++; } +static bool dcp_present_begin(struct apple_dcp *d,u32 id) { (void)d;assert(id);return begin_allowed; } +static void dcp_arm_retained_framebuffers(struct apple_dcp *d,u32 id) { (void)d;(void)id;armed++; } +static void dcp_h17p_prepare_swap(struct apple_dcp *d) { assert(!d->native); } +static void dcp_swap_submit(struct apple_dcp *d,bool oob,void *r,void (*cb)(struct apple_dcp *,void *,void *),void *c) { (void)d;(void)r;(void)cb;(void)c;assert(!oob);sent++; } +''' +check=r''' +static struct apple_dcp fresh(bool native) { + struct apple_dcp d={.native=native};d.swap.h17p.swap.swap_id=7; + failures=events=armed=watchdogs=completed=sent=0;submit_result=0;begin_allowed=true;return d; +} +int main(void) { + struct dcp_swap_submit_resp bad={1},ok={0};struct apple_dcp d=fresh(true); + dcp_swapped(&d,&bad,NULL);assert(d.crashed&&d.ext26_blocked&&failures==1&&events==1&&!watchdogs); + d=fresh(false);dcp_swapped(&d,&bad,NULL);assert(!d.crashed&&!d.ext26_blocked&&failures==1); + d=fresh(true);dcp_swapped(&d,NULL,NULL);assert(d.crashed&&d.ext26_blocked&&!watchdogs); + d=fresh(true);submit_result=-ECANCELED;dcp_swapped(&d,&ok,NULL);assert(d.crashed&&d.ext26_blocked); + d=fresh(true);d.ext26_early_complete=true;d.ext26_early_id=7;d.ext26_early_ns=15;d.ext26_rx_ns=20; + dcp_swapped(&d,&ok,NULL);assert(completed==1&&completion_time==15&&d.ext26_rx_ns==20&&!d.ext26_early_complete&&!watchdogs&&!d.crashed); + d=fresh(true);dcp_swapped(&d,&ok,NULL);assert(watchdogs==1&&!d.crashed&&!failures); + struct dcp_swap_start_resp start={8,1};d=fresh(true);dcp_swap_started(&d,&start,NULL);assert(d.crashed&&d.ext26_blocked&&!armed&&!sent); + d=fresh(false);dcp_swap_started(&d,&start,NULL);assert(!d.crashed&&!sent); + d=fresh(true);start=(struct dcp_swap_start_resp){0,0};dcp_swap_started(&d,&start,NULL);assert(d.crashed&&!sent); + d=fresh(true);start.swap_id=8;begin_allowed=false;dcp_swap_started(&d,&start,NULL);assert(d.crashed&&!sent&&!armed); + d=fresh(true);dcp_swap_started(&d,&start,NULL);assert(!d.crashed&&sent==1&&armed==1&&d.swap.h17p.swap.swap_id==8); + puts("PASS actual native DRM start/submit rejection, classic controls and original-time early D590 joining"); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'check.c').write_text(pre+'\n'.join(function(n) for n in ['dcp_present_rejected','dcp_swapped','dcp_swap_started'])+check) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-O1','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-fsanitize=address,undefined',str(p/'check.c'),'-o',str(p/'check')],check=True) + subprocess.run([str(p/'check')],check=True) diff --git a/tools/asahi/dcpext26-profile-tests.py b/tools/asahi/dcpext26-profile-tests.py new file mode 100644 index 00000000000000..ca8c29b4cff6e7 --- /dev/null +++ b/tools/asahi/dcpext26-profile-tests.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Native26 callback/startup/readiness helpers with a synthetic peer boundary.""" +from pathlib import Path +import os +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +source = (ROOT / 'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +header = (ROOT / 'drivers/gpu/drm/apple/iomfb.h').read_text() + + +def function(name): + start = source.index('static ', source.index(name) - 40) + # Find the signature's start without relying on the return type's line wrapping. + pos = source.index(name + '(') + start = source.rfind('static ', 0, pos) + return source[start:source.index('\n}', pos)+2] + + +def block(text, name): + start = text.index(name) + return text[start:text.index('\n};', start)+3] + + +pre = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; +#define GFP_KERNEL 0 +#define kzalloc_obj(v) calloc(1,sizeof(v)) +#define kfree free +#define READ_ONCE(v) (v) +#define WRITE_ONCE(v,x) ((v)=(x)) +#define dev_err(...) ((void)0) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +static u32 get_unaligned_le32(const void *p) { const u8 *b=p; return (u32)b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } +static void put_unaligned_le32(u32 v,void *p) { u8 *b=p;for(unsigned i=0;i<4;i++) b[i]=v>>(8*i); } +static void put_unaligned_le64(u64 v,void *p) { u8 *b=p;for(unsigned i=0;i<8;i++) b[i]=v>>(8*i); } +struct mock_connector { bool connected; }; +struct apple_dcp { struct mock_connector *connector; bool crashed, ext26_normal, ext26_power_sent; u64 ext26_rx_ns,ext26_power_ns,ext26_generation,typec_generation; int ext26_ready; }; +static unsigned withdrawn,invalidated,notifications; +static void dcp_modes_begin_attachment(struct apple_dcp *d) { (void)d;withdrawn++; } +static void apple_connector_invalidate_edid(struct mock_connector *c) { (void)c;invalidated++; } +static void dcp_queue_hotplug(struct mock_connector *c) { (void)c;notifications++; } +static void complete_all(int *c) { (*c)++; } +static bool iomfb_validate_callback_h17p(int t,u32 i,u32 o) { (void)t;(void)i;(void)o;return false; } +struct dcp_method_entry { const char *name; char tag[4],tag_h17g[4],tag_ext26[4]; }; +#define IOMFB_METHOD(t,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{0}} +#define IOMFB_METHOD_H17(t,h,n) [n]={#n,{t[0],t[1],t[2],t[3]},{h[0],h[1],h[2],h[3]},{0}} +#define IOMFB_METHOD_EXT26(t,e,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{e[0],e[1],e[2],e[3]}} +static unsigned count,done,fault,bad_status; +static void boot_done(struct apple_dcp *d,void *o,void *c) { (void)d;(void)o;(void)c;done++; } +static void dcp_push(struct apple_dcp *,bool,const struct dcp_method_entry *,u32,u32,void *,void (*)(struct apple_dcp *,void *,void *),void *); +''' +peer = r''' +static void dcp_push(struct apple_dcp *d,bool oob,const struct dcp_method_entry *m,u32 in,u32 out,void *input,void (*next)(struct apple_dcp *,void *,void *),void *cookie) { + const char *expected[]={"A389","A446","A029","A468","A000","A465"}; + const u32 ins[]={0,0,0,4,4,0},outs[]={0,4,0,0,4,4}; + assert(count<6 && !oob); + const char *tag=m->tag_ext26[0]?m->tag_ext26:m->tag; + assert(!memcmp(tag,expected[count],4)); + assert(in==ins[count] && out==outs[count]); + if(in) assert(get_unaligned_le32(input)==1); + u32 reply=count>=4 ? 1 : 0; + count++; + if (bad_status==count) reply=0; + next(d,fault==count?NULL:&reply,cookie); +} +int main(void) { + struct apple_dcp d={0};struct ext26_boot *b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(done==1 && count==6 && !d.crashed); + for(fault=1;fault<=6;fault++) { + d=(struct apple_dcp){0};done=count=0;b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(!done && d.crashed && count==fault); + } + fault=0; + for(bad_status=5;bad_status<=6;bad_status++) { + d=(struct apple_dcp){0};done=count=0;b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(!done && d.crashed && count==bad_status); + } + unsigned char input[128]={0}, output[128]; + memset(output,0xcd,sizeof(output));ext26_bandwidth(&d,3,output+1,input); + for(unsigned i=0;i<60;i++) assert(output[i+1]==(i==56)); + assert(output[0]==0xcd && output[61]==0xcd); + for(unsigned i=0;i<80;i++) input[i]=i; + input[80]=0;ext26_frame_sync(&d,6,output,input);assert(!memcmp(output,input,80)); + input[80]=1;ext26_frame_sync(&d,6,output,input); + for(unsigned i=0;i<80;i++) assert(!output[i]); + memcpy(input,"VORP",4);put_unaligned_le32(0,input+4);ext26_clock(&d,408,output,input); + assert(get_unaligned_le32(output)==48000000); + put_unaligned_le32(1,input+4);ext26_clock(&d,408,output,input);assert(get_unaligned_le32(output)==24000000); + put_unaligned_le32(2,input+4);ext26_clock(&d,408,output,input);assert(!get_unaligned_le32(output)); + d=(struct apple_dcp){.ext26_power_sent=true,.ext26_power_ns=10,.ext26_generation=7,.typec_generation=7}; + memset(input,0,sizeof(input));memcpy(input,"NormalModeActive",17);input[64]=1; + for(unsigned t=0;t<=10;t++) { d.ext26_rx_ns=t;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal && !d.ext26_ready); } + d.ext26_rx_ns=11;ext26_property(&d,104,NULL,input);assert(d.ext26_normal && d.ext26_ready==1); + input[64]=0;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + input[64]=1;d.typec_generation++;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + d.typec_generation--;d.ext26_power_sent=false;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + d.ext26_power_sent=true;ext26_property(&d,104,NULL,input);assert(d.ext26_normal); + ext26_property(&d,107,NULL,input);assert(!d.ext26_normal); + struct mock_connector connector={true};d.connector=&connector; + memset(input,0,sizeof(input));memcpy(input,"TimingElements",15);ext26_property(&d,107,NULL,input); + assert(withdrawn==1&&invalidated==1&¬ifications==1&&!connector.connected); + const unsigned tags[]={3,6,107,115,116,134,408,454,567,572,576,588}; + const unsigned ins[]={4,84,64,16,16,0,8,4,128,4,4,0},outs[]={60,80,0,8,4,4,8,4,4,4,0,0}; + for(unsigned i=0;i deferred OPEN, and nested D121 transport regressions. + +Extract production C bodies. Firmware/queues are peers, not hardware proof. +An optional --capture JSON supplies bounded logged prefixes, never fabricated +complete captures. Unlogged dictionaries below are explicitly synthetic. +""" +from pathlib import Path +import argparse,json,os,re,subprocess,tempfile +R=Path(__file__).resolve().parents[2];A=R/'drivers/gpu/drm/apple' +def fn(s,name): + matches=list(re.finditer(r'(?m)^[^\n;{}]*\b'+name+r'\([^;{}]*\)\n\{',s)) + if not matches: raise RuntimeError('missing body '+name) + m=matches[0];start=m.start() + if s[start:m.end()].lstrip().startswith(name+'('):start=s.rfind('\n',0,start-1)+1 + end=m.end();depth=1 + while depth:depth+=(s[end]=='{')-(s[end]=='}');end+=1 + return s[start:end] +def block(s,name): + start=s.index(name);end=s.index('\n};',start)+3;return s[start:end] +def run(name,code,reject=False): + with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-Wno-unused-function','-Wno-unused-parameter','-Wno-sign-compare','-Wno-missing-field-initializers','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + result=subprocess.run([str(p.with_suffix(''))],capture_output=reject,check=False) + if reject: + if result.returncode == 0:raise RuntimeError('mutant survived: '+name) + else:result.check_returncode() + print('PASS',name,flush=True) +args=argparse.ArgumentParser();args.add_argument('--capture',type=Path);args.add_argument('--mutants',action='store_true');args=args.parse_args() +pre=r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8;typedef uint16_t u16;typedef uint32_t u32;typedef uint64_t u64;typedef int64_t s64;typedef uint32_t __le32; +#define __packed __attribute__((packed)) +#define GFP_KERNEL 0 +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define round_up(x,a) (((x)+(a)-1)&~((a)-1)) +#define ALIGN round_up +#define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) +#define IS_ERR_OR_NULL(p) (!(p)||IS_ERR(p)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define ERR_PTR(e) ((void *)(intptr_t)(e)) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +#define BIT(n) (1UL<<(n)) +#define U32_MAX UINT32_MAX +#define S64_MIN INT64_MIN +#define S64_MAX INT64_MAX +#define kmalloc(n,f) malloc(n) +#define kzalloc_obj(v) calloc(1,sizeof(v)) +#define kfree(p) free((void *)(p)) +#define max(a,b) ((a)>(b)?(a):(b)) +#define dev_info(...) ((void)0) +#define dev_err(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_warn_ratelimited(...) ((void)0) +#define EXPORT_SYMBOL_IF_KUNIT(...) +#define spin_lock_irqsave(l,f) ((f)=0) +#define spin_unlock_irqrestore(l,f) ((void)(f)) +static void put_unaligned_le32(u32 v,void *p){memcpy(p,&v,4);} +static u32 get_unaligned_le32(const void *p){u32 v;memcpy(&v,p,4);return v;} +static u64 get_unaligned_le64(const void *p){u64 v;memcpy(&v,p,8);return v;} +''' +parser=(A/'parser.c').read_text();afk=(A/'afk.c').read_text();dptx=(A/'dptxep.c').read_text();dcp=(A/'dcp.c').read_text();h=(A/'afk.h').read_text() +codec=re.sub(r'^#include[^\n]*\n','',(A/'afkv2-codec.h').read_text(),flags=re.M) +identity=pre+codec+r''' +#define DCP_FIRMWARE_H17P 1 +#define DCP_FIRMWARE_V_14_7 2 +#define AFK_MAX_CHANNEL 16 +#define SYSTEM_ENDPOINT 0x20 +#define DISP0_ENDPOINT 0x23 +#define EPIC_TYPE_NOTIFY 0 +#define EPIC_CAT_REPORT 0 +#define bitmap_empty(p,n) 1 +#define MAX_PENDING_CMDS 16 +struct dcp_parse_ctx {void *dcp;const void *blob;u32 pos,len;}; +struct apple_epic_service;struct apple_dcp_afkep; +struct apple_epic_service_ops {const char *name;bool reusable,optional;int (*prepare)(struct apple_epic_service *,const char *,const char *,s64);int (*revalidate)(struct apple_epic_service *,const char *,const char *,s64);void (*init)(struct apple_epic_service *,const char *,const char *,s64);void (*teardown)(struct apple_epic_service *);}; +struct apple_epic_service {struct apple_dcp_afkep *ep;const struct apple_epic_service_ops *ops;bool enabled,torndown,pending_open;unsigned users;int lock;u32 channel;void *cookie;const char *pending_name,*pending_class;s64 pending_unit;int cmd_map[1];}; +struct dptx_port {struct apple_epic_service *service;s64 unit;bool enabled,connected;int enable_completion;}; +struct apple_dcp {bool native,ext26_services_open;int fw_compat,index,dptx_controller_ready;struct {unsigned num_dptx_ports;} hw;struct dptx_port dptxport[2];void *dev;struct apple_dcp_afkep *systemep,*ibootep,*dptx_controller_ep,*dcpavservep,*dptxep,*ext26_aux[3];}; +struct apple_dcp_afkep {struct apple_dcp *dcp;u32 endpoint,num_channels;bool wire_v2,stopping,match_epic_name;int lock;void *wq,*service_work;const struct apple_epic_service_ops *ops;struct apple_epic_service services[AFK_MAX_CHANNEL];}; +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static bool dcp_is_v14_7_external(struct apple_dcp *d){return false;} +static bool dcp_fw_is_14_7(struct apple_dcp *d){return d->fw_compat==DCP_FIRMWARE_V_14_7;} +static size_t strscpy(char *d,const void *v,size_t n){const char *s=v;size_t l=strnlen(s,n-1);memcpy(d,s,l);d[l]=0;return l;} +static void complete(int *c){(*c)++;} +#define trace_dptxport_init(...) +#define smp_store_release(p,v) (*(p)=(v)) +static unsigned opened[256][16],queued,published; +static int open_error; +static void afk_populate_service_debugfs(struct apple_epic_service *s){} +static void afk_recv_handle_teardown(struct apple_dcp_afkep *e,u32 c){} +static void afk_retry_service_init(struct apple_dcp_afkep *ep); +void afk_service_retry(struct apple_dcp_afkep *ep); +bool dptx_native_controllers_ready(struct apple_dcp *dcp); +static int queue_work(void *wq,void *work){struct apple_dcp_afkep *e=wq;assert(e->dcp->ext26_services_open||!e->stopping);queued++;afk_retry_service_init(e);return 1;} +static int afk_send_epic(struct apple_dcp_afkep *e,u32 ch,u16 tag,int ty,int cat,u8 sub,const void *p,size_t n){assert(sub==AFKV2_OPEN&&!n&&ch<16&&e->dcp->ext26_services_open);opened[e->endpoint][ch]++;return open_error;} +''' +identity+=parser[parser.index('#define DCP_PARSE_HEADER'):parser.index('static int parse_bool(')] +# Drop the audio-only helper region from the extracted prefix. +identity=re.sub(r'#if IS_ENABLED\(CONFIG_DRM_APPLE_AUDIO\).*?#endif','',identity,flags=re.S) +identity+=block(parser,'struct iterator {')+'\n'+fn(parser,'iterator_begin')+'\n'+fn(parser,'parse')+'\n'+fn(parser,'parse_epic_service_init')+'\n' +for name in ['afk_service_reclaimable','afk_service_reinit','afk_service_discard','afk_service_publish','afk_service_matches']: + identity+=fn(h,name)+'\n' +identity+=fn(dptx,'dptx_native_controllers_ready')+'\n'+fn(dptx,'dptx_native_controller_prepare')+'\n'+fn(dptx,'dptx_controller_service_init')+'\n' +for name in ['afk_match_service','afk_epic_find_service','afk_discard_pending_open','afk_retry_service_init','afk_service_retry']: + identity+=fn(afk,name)+'\n' +service=(A/'dptx-service.h').read_text() +for name in ['dptx_service_port','dptx_service_bind','dptx_service_publish']: + identity+=fn(service,name)+'\n' +identity+=fn(dptx,'dptxport_prepare')+'\n'+fn(dcp,'dcp_ext26_open_services')+'\n'+fn(afk,'afk_recv_handle_init')+r''' +static void port_init(struct apple_epic_service *s,const char *n,const char *c,s64 u){dptx_service_publish(s);published++;} +static const struct apple_epic_service_ops port_ops[]={{.name="AppleDCPDPTXRemotePort",.prepare=dptxport_prepare,.init=port_init},{0}}; +static const struct apple_epic_service_ops controller_ops[]={{.name="AppleDCPDPTXController",.prepare=dptx_native_controller_prepare,.init=dptx_controller_service_init},{0}}; +static const struct apple_epic_service_ops av_ops[]={{.name="DCPDPService"},{0}}; +struct blob {u8 p[2048];size_t n;}; +static void tag(struct blob *b,u32 t,u32 n){b->n=ALIGN(b->n,4);put_unaligned_le32(t<<24|n,b->p+b->n);b->n+=4;} +static void text(struct blob *b,const char *s){tag(b,9,strlen(s));memcpy(b->p+b->n,s,strlen(s));b->n+=strlen(s);} +static void number(struct blob *b,s64 v){tag(b,4,64);memcpy(b->p+b->n,&v,8);b->n+=8;} +static struct blob description(const char *name,const char *class,s64 unit,u32 channel,bool alias_first){ + struct blob b={0};memcpy(b.p,"dispext0",9);b.n=32;put_unaligned_le32(0xd3,b.p+b.n);b.n+=4;tag(&b,1,4); + if(alias_first){text(&b,"interface-id");number(&b,channel);} + text(&b,"EPICName");text(&b,name);text(&b,"EPICProviderClass");text(&b,class);text(&b,"EPICUnit");number(&b,unit); + if(!alias_first){text(&b,"interface-id");number(&b,channel);}return b; +} +static void announce(struct apple_dcp_afkep *e,unsigned ch,struct blob b){ + u8 record[4096];size_t size;struct afkv2_frame f; + assert(!afkv2_encode_frame(record,sizeof(record),0,ch,0,0x11,AFKV2_REPORT,4,b.p,b.n,&size)); + assert(!afkv2_decode_frame(record,size,false,&f));afk_recv_handle_init(e,f.interface,(void *)f.payload,f.payload_size); +} +int main(void){ + struct apple_dcp d={.native=true,.fw_compat=DCP_FIRMWARE_H17P,.index=1,.hw.num_dptx_ports=2};struct apple_dcp_afkep eps[8]={0}; + struct apple_dcp_afkep **slots[]={&d.systemep,&d.ibootep,&d.dptx_controller_ep,&d.ext26_aux[0],&d.dcpavservep,&d.ext26_aux[1],&d.dptxep,&d.ext26_aux[2]}; + const unsigned ids[]={0x20,0x23,0x24,0x27,0x28,0x29,0x2a,0x2c}; + for(unsigned i=0;i<8;i++){*slots[i]=&eps[i];eps[i]=(struct apple_dcp_afkep){.dcp=&d,.endpoint=ids[i],.wire_v2=true};eps[i].wq=&eps[i];} + d.dptxep->ops=port_ops;d.dptx_controller_ep->ops=controller_ops;d.dcpavservep->ops=av_ops; + // Infrastructure dictionaries have interface metadata but no EPIC identity, as on the working daily run. + struct blob infrastructure={0};memcpy(infrastructure.p,"system",7);infrastructure.n=32; + put_unaligned_le32(0xd3,infrastructure.p+32);infrastructure.n+=4;tag(&infrastructure,1,2); + text(&infrastructure,"interface-id");number(&infrastructure,1);text(&infrastructure,"ep-has-desc-mgr");tag(&infrastructure,11,1); + announce(d.systemep,1,infrastructure);announce(d.ibootep,1,infrastructure);assert(!d.systemep->num_channels&&!d.ibootep->num_channels); + // Both alias orders, distinct interface and EPICUnit, pass through production AFK/parser/prepare. + announce(d.dptxep,1,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,1,false)); + announce(d.dptxep,3,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",1,3,true)); + assert(d.dptxep->num_channels==2&&d.dptxport[0].unit==0&&d.dptxport[1].unit==1); + assert(!d.dptxport[0].service&&!d.dptxport[1].service); + for(unsigned i=0;i<4;i++)announce(d.dptx_controller_ep,2*i+1,description(i&1?"dcpdp-controller-epic":"dcpav-controller-epic","AppleDCPDPTXController",i/2,2*i+1,false)); + announce(d.dcpavservep,1,description("dcpav-service-epic","DCPDPService",0,1,false)); + // No service becomes visible and no OPEN goes out before A401. + assert(!published);for(unsigned i=0;i<8;i++)for(unsigned c=0;c<16;c++)assert(!opened[ids[i]][c]); + d.ext26_services_open=true;afk_service_retry(d.dptxep);assert(!published&&!opened[0x2a][1]); + queued=0;dcp_ext26_open_services(&d);assert(queued==9&&published==2&&dptx_native_controllers_ready(&d)); + assert(d.dptxport[0].service&&d.dptxport[1].service&&d.dptxport[0].enabled&&d.dptxport[1].enabled); + assert(opened[0x2a][1]==1&&opened[0x2a][3]==1&&opened[0x28][1]==1); + for(unsigned i=0;i<4;i++)assert(opened[0x24][2*i+1]==1); + dcp_ext26_open_services(&d);assert(published==2&&opened[0x2a][1]==1); + d.dptx_controller_ep->services[0].enabled=false;assert(!dptx_native_controllers_ready(&d));d.dptx_controller_ep->services[0].enabled=true; + // Native invalid units still fail closed; truncated identities publish nothing. + announce(d.dptxep,5,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",3,5,false));assert(!opened[0x2a][5]); + struct blob short_b={0};short_b.n=1;announce(d.dptxep,7,short_b);assert(!opened[0x2a][7]); + // Panel aliases retain their existing meaning; native explicit EPIC keys do not consume interface-id. + struct blob b=description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,3,false);struct dcp_parse_ctx ctx;const char *n,*c;s64 u=-1; + assert(!parse(b.p+32,b.n-32,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,false)&&u==0);free((void *)n);free((void *)c); + assert(!parse(b.p+32,b.n-32,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,true)&&u==3);free((void *)n);free((void *)c); + // Preserve panel-only lowercase aliases, which native26 intentionally does not accept. + struct blob panel={0};put_unaligned_le32(0xd3,panel.p);panel.n=4;tag(&panel,1,3); + text(&panel,"compartment");text(&panel,"system");text(&panel,"name");text(&panel,"system");text(&panel,"interface-id");number(&panel,1); + assert(!parse(panel.p,panel.n,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,true)&&u==1&&!strcmp(n,"system"));free((void *)n);free((void *)c); + assert(!parse(panel.p,panel.n,&ctx));assert(parse_epic_service_init(&ctx,&n,&c,&u,false)==-ENOENT); + // A failed OPEN cannot publish a prepared port; closed workqueues are never retried. + struct apple_dcp_afkep failed={.dcp=&d,.endpoint=0x2a,.wire_v2=true,.ops=port_ops};failed.wq=&failed; + d.dptxport[0].service=NULL;d.dptxport[0].enabled=false;open_error=-EIO;announce(&failed,1,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,1,false));assert(!d.dptxport[0].service&&!failed.services[0].enabled); + failed.stopping=true;unsigned q=queued;afk_service_retry(&failed);assert(queued==q); + d.native=false;failed.stopping=false;afk_service_retry(&failed);assert(queued==q); // classic only retries its EDID endpoint + for(unsigned i=0;i<8;i++)for(unsigned j=0;j A401 queues all eight endpoints -> exact once OPEN and publication; aliases, truncation, bad units, transport failure and classic retry guard"); +} +''' +run('native AFK announcements through deferred OPEN',identity) +# Compile the actual command allocator and ACK dispatch with the production D121 +# callback. A peer can write one Boolean byte or all four bytes, then nest D003. +iom=(A/'iomfb.c').read_text();native=(A/'iomfb_h17p.c').read_text();ih=(A/'iomfb.h').read_text();internal=(A/'dcp-internal.h').read_text() +transport=pre+r''' +#define DCP_FIRMWARE_H17P 1 +#define DCP_IOMFB_METHODS_H17G 2 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 3 +#define DCP_MAX_CALL_DEPTH 8 +#define IOMFB_MAX_CB 600 +#define DCP_PACKET_ALIGNMENT (0x40) +#define IOMFB_ENDPOINT 0x37 +#define IOMFB_MESSAGE_TYPE 15ULL +#define IOMFB_MESSAGE_TYPE_MSG 2 +#define IOMFB_MSG_CONTEXT 0xf00ULL +#define IOMFB_MSG_OFFSET 0xffff0000ULL +#define IOMFB_MSG_LENGTH 0xffffffff00000000ULL +#define IOMFB_MSG_ACK 64ULL +#define FIELD_PREP(m,v) (((u64)(v)<<__builtin_ctzll(m))&(m)) +#define FIELD_GET(m,v) (((v)&(m))>>__builtin_ctzll(m)) +#define WARN_ON(x) assert(!(x)) +#define IS_ENABLED(x) 0 +#define current ((void *)1) +#define dma_wmb() ((void)0) +#define dma_rmb() ((void)0) +#define trace_iomfb_push(...) +#define IOMFB_METHOD(t,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{0}} +#define IOMFB_METHOD_H17(t,h,n) [n]={#n,{t[0],t[1],t[2],t[3]},{h[0],h[1],h[2],h[3]},{0}} +#define IOMFB_METHOD_EXT26(t,e,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{e[0],e[1],e[2],e[3]}} +struct apple_dcp;typedef void (*dcp_callback_t)(struct apple_dcp *,void *,void *); +struct dcp_method_entry {const char *name;char tag[4],tag_h17g[4],tag_ext26[4];}; +''' +transport+=block(ih,'enum dcp_context_id {')+'\n'+block(ih,'struct dcp_packet_header {')+'\n'+block(internal,'struct dcp_channel {')+'\n'+block(ih,'enum dcpep_method {')+r''' +struct apple_dcp {void *dev;u8 *shmem;int fw_compat;bool native,crashed,quiescing,ext26_blocked,ext26_power_sent;u64 ext26_power_ns;struct {int iomfb_method_profile;}hw;struct {void *owner;} iomfb;struct dcp_channel ch_cb,ch_cmd,ch_oobcb,ch_oobcmd,ch_async,ch_oobasync;}; +static bool iomfb_uses_queue(struct apple_dcp *d){return true;} +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static int iomfb_queue_command(struct apple_dcp *d,bool o,const struct dcp_method_entry *m,u32 i,u32 n,void *p,dcp_callback_t cb,void *c){abort();} +static u64 ktime_get_ns(void){return 1;} +static void dcp_send_message(struct apple_dcp *,u8,u64); +static void dcp_ack(struct apple_dcp *,enum dcp_context_id); +static void dcp_push(struct apple_dcp *,bool,const struct dcp_method_entry *,u32,u32,void *,dcp_callback_t,void *); +static void boot_done(struct apple_dcp *,void *,void *); +static unsigned commands,callback_acks;static int bad_status,corrupt;static bool byte_reply;static u8 memory[0x100000]; +''' +for name in ['dcp_tx_offset','dcp_call_context','dcp_get_channel','dcp_packet_start','dcp_push_depth','dcp_pop_depth','dcpep_msg','dcpep_ack','dcp_push','dcp_ack','dcpep_handle_ack']: + code=fn(iom,name) + if name in ['dcp_push','dcp_ack']:code='static '+code + transport+=code+'\n' +transport+=block(native,'static const struct dcp_method_entry dcp_methods[')+'\n'+block(native,'struct ext26_boot {')+'\n' +for name in ['ext26_boot_next','ext26_boot_callback','ext26_bandwidth']: + transport+=fn(native,name)+'\n' +transport+=fn((A/'iomfb_template.c').read_text(),'boot_done')+r''' +static void callback(struct apple_dcp *d,int tag,unsigned in,unsigned out,unsigned off){ + struct dcp_channel *ch=&d->ch_cb;u8 *record=d->shmem+0x60000+off; + // Firmware-originated callback record; production handlers and ACK share this backing. + memset(record,0,12+in+out);struct dcp_packet_header *h=(void *)record;h->in_len=in;h->out_len=out; + if(tag==3){u8 raw[4]={0x00,0x0b,0x20,0xf1};memcpy(record+12,raw,4);} // daily D003 input, captured in full + unsigned depth=dcp_push_depth(&ch->depth);ch->output[depth]=record+12+in;ch->end[depth]=off+ALIGN(12+in+out,64); + bool ack=tag==121?ext26_boot_callback(d,tag,record+12+in,record+12):ext26_bandwidth(d,tag,record+12+in,record+12); + if(ack)dcp_ack(d,DCP_CONTEXT_CB); +} +static void dcp_send_message(struct apple_dcp *d,u8 ep,u64 message){ + assert(ep==IOMFB_ENDPOINT);enum dcp_context_id context=FIELD_GET(IOMFB_MSG_CONTEXT,message); + if(message&IOMFB_MSG_ACK){callback_acks++;return;} + unsigned offset=FIELD_GET(IOMFB_MSG_OFFSET,message);struct dcp_packet_header *h=(void *)(d->shmem+dcp_tx_offset(context)+offset);u8 *in=(u8 *)(h+1),*out=in+h->in_len; + const char *tags[]={"983A","644A","920A","864A","000A","564A"};unsigned inputs[]={0,0,0,4,4,0},outputs[]={0,4,0,0,4,4}; + assert(commands<6&&!memcmp(h->tag,tags[commands],4)&&h->in_len==inputs[commands]&&h->out_len==outputs[commands]); + assert(context==DCP_CONTEXT_CB&&offset>=64);if(h->in_len)assert(get_unaligned_le32(in)==1); + unsigned call=++commands; + if(call==5){callback(d,3,4,60,64);assert(get_unaligned_le32(d->shmem+0x60000+64+12+4+56)==1);} + if(h->out_len){u32 value=call>=5?1:0;if(bad_status==call)value=0;if(byte_reply)*out=value;else put_unaligned_le32(value,out);} + if(corrupt==call)h->tag[0]^=1; + dcpep_handle_ack(d,context); +} +int main(void){ + for(unsigned full=0;full<2;full++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;byte_reply=!full; + callback(&d,121,0,4,0);assert(!d.crashed&&commands==6&&callback_acks==2&&!d.ch_cb.depth&&get_unaligned_le32(memory+0x60000+12)==1); + } + for(bad_status=5;bad_status<=6;bad_status++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;byte_reply=true;callback(&d,121,0,4,0);assert(d.crashed&&commands==bad_status&&callback_acks==1&&d.ch_cb.depth==1); + } + bad_status=0; + for(corrupt=1;corrupt<=6;corrupt++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;callback(&d,121,0,4,0);assert(d.crashed&&commands==corrupt); + } + puts("actual dcp_push/shared memory/ACK/D121 nested D003: Boolean byte and word replies pass, semantic refusal and corrupt command records stop successors and retain the session"); +} +''' +run('native D121 through actual shared-memory command and ACK bodies',transport) +if args.capture: + capture=json.loads(args.capture.read_text());assert capture['candidate_raw_messages_logged'] is False + for record in capture['daily_afk_prefixes']: + b=bytes.fromhex(record['hex']);assert b[:4]==b'IOP ' and len(b)==128 + declared=int.from_bytes(b[20:24],'little')+8 + assert len(b)-16dcp->fw_compat == DCP_FIRMWARE_H17P &&\s*!dcp_is_ext26\(ep->dcp\)', 'ep->dcp->fw_compat == DCP_FIRMWARE_H17P', identity) + if old_alias==identity:raise RuntimeError('identity mutation missed') + run('reject restored panel interface-id alias on native port',old_alias,True) + old_retry=identity.replace('(!dcp_is_ext26(ep->dcp) && ep != ep->dcp->dcpavservep)', 'ep != ep->dcp->dcpavservep') + if old_retry==identity:raise RuntimeError('retry mutation missed') + run('reject restored EDID-only deferred OPEN queue',old_retry,True) + old_padding=transport.replace('dcp_is_ext26(dcp) ? 0 : 0xff', '0xff') + if old_padding==transport:raise RuntimeError('padding mutation missed') + run('reject restored native all-ones Boolean reply padding',old_padding,True) diff --git a/tools/asahi/dcpext26-wire-tests.py b/tools/asahi/dcpext26-wire-tests.py new file mode 100644 index 00000000000000..ea465f215f33e4 --- /dev/null +++ b/tools/asahi/dcpext26-wire-tests.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Execute the production native26 encoder against independent byte fixtures.""" +import ctypes as C +import os +from pathlib import Path +import struct +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +SIZE = 0x1BD8 + + +class Primary(C.Structure): + _fields_ = [("address", C.c_uint64), ("backing", C.c_uint64), + ("width", C.c_uint32), ("height", C.c_uint32), ("pitch", C.c_uint32)] + + +def fixture(layer, address, width, height, pitch, swap_id=17): + """Byte schema, including every zero byte, independent of C field writes.""" + fields = {0x98: struct.pack('\n#include \ntypedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64;\n', + 'errno.h': '#ifdef __linux__\n#include \n#else\n#include \n#endif\n', 'string.h': '#include \n', + 'unaligned.h': '''#include +#include +static inline void put_unaligned_le32(uint32_t v, void *p) { + unsigned char *b=p; for (unsigned i=0;i<4;i++) b[i]=v>>(8*i); +} +static inline void put_unaligned_le64(uint64_t v, void *p) { + unsigned char *b=p; for (unsigned i=0;i<8;i++) b[i]=v>>(8*i); +} +static inline uint32_t get_unaligned_le32(const void *p) { + const unsigned char *b=p; uint32_t v=0; + for (unsigned i=0;i<4;i++) { v|=(uint32_t)b[i]<<(8*i); } + return v; +} +''', + }.items(): + (p / 'linux' / name).write_text(body) + (p / 'test.c').write_text('''#include "dcpext26-wire.h" +int encode(void *b, size_t n, u32 id, unsigned layer, const struct dcpext26_primary *p) { + return dcpext26_primary_encode(b,n,id,layer,p); +} +int start(const void *r,size_t n,u32 *id) { return dcpext26_start_reply(r,n,id); } +int submit(const void *r,size_t n) { return dcpext26_submit_reply(r,n); } +''') + exe = p / 'wire.so' + subprocess.run([os.environ.get('CC', 'cc'), '-shared', '-fPIC', '-O2', + '-Wall', '-Wextra', '-Werror', '-I' + str(p), + '-I' + str(ROOT / 'drivers/gpu/drm/apple'), str(p / 'test.c'), + '-o', str(exe)], check=True) + cls.lib = C.CDLL(str(exe)) + cls.lib.encode.argtypes = [C.c_void_p, C.c_size_t, C.c_uint32, + C.c_uint, C.POINTER(Primary)] + cls.lib.start.argtypes = [C.c_void_p, C.c_size_t, C.POINTER(C.c_uint32)] + cls.lib.submit.argtypes = [C.c_void_p, C.c_size_t] + + @classmethod + def tearDownClass(cls): + cls.temp.cleanup() + + def test_complete_surface_records(self): + for layer in range(4): + for w, h, pitch in [(3840, 2160, 15360), (1920, 1080, 8192), (1, 1, 4)]: + p = Primary(0x12345678000, pitch * h + 4096, w, h, pitch) + b = C.create_string_buffer(SIZE + 2) + C.memset(b, 0xCD, SIZE + 2) + self.assertEqual(self.lib.encode(C.byref(b, 1), SIZE, 17, layer, C.byref(p)), 0) + self.assertEqual(b.raw[1:-1], fixture(layer, p.address, w, h, pitch)) + self.assertEqual(b.raw[0], 0xCD) + self.assertEqual(b.raw[-1], 0xCD) + + def test_null_surface(self): + b = C.create_string_buffer(SIZE) + self.assertEqual(self.lib.encode(b, SIZE, 17, 0, None), 0) + self.assertEqual(b.raw, fixture(0, 0, 0, 0, 0)) + + def test_invalid_inputs_preserve_buffer(self): + good = (0x100000, 33177600, 3840, 2160, 15360) + cases = [(0, *good[1:]), (good[0], 4, *good[2:]), + (*good[:2], 0, *good[3:]), (*good[:3], 0, good[4]), + (*good[:4], 15359), (*good[:4], 16), + (2**64-4, *good[1:]), (good[0], 2**64-1, 1, 2**32-1, 8)] + for values in cases: + b = C.create_string_buffer(b'\xcd' * SIZE, SIZE) + p = Primary(*values) + self.assertLess(self.lib.encode(b, SIZE, 17, 0, C.byref(p)), 0) + self.assertEqual(b.raw, b'\xcd' * SIZE) + p = Primary(*good) + for length, swap_id, layer in [(SIZE-1, 17, 0), (SIZE+1, 17, 0), + (SIZE, 0, 0), (SIZE, 17, 4), (SIZE, 17, 2**32-1)]: + b = C.create_string_buffer(b'\xcd' * SIZE, SIZE) + self.assertLess(self.lib.encode(b, length, swap_id, layer, C.byref(p)), 0) + self.assertEqual(b.raw, b'\xcd' * SIZE) + + def test_reply_lengths_and_status_offsets(self): + ident = C.c_uint32(0xFFFFFFFF) + for n in range(20): + start = C.create_string_buffer(struct.pack(' +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; +#define BIT(x) (1U<<(x)) +#define BIT_ULL(x) (1ULL<<(x)) +#define __ffs64(x) __builtin_ctzll(x) +static u32 get_unaligned_le32(const void *p) { const u8 *b=p; return (u32)b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } +static u64 get_unaligned_le64(const void *p) { return get_unaligned_le32(p)|(u64)get_unaligned_le32((const u8 *)p+4)<<32; } +static void put32(u32 v,void *p) { u8 *b=p; for(unsigned i=0;i<4;i++) b[i]=v>>(8*i); } +struct device_node { bool available; struct device_node *report; }; +struct device { struct device_node *of_node; }; +struct apple_pmp_report { struct device *dev; struct device_node *native_pmp; }; +static struct device_node chosen, report, suppliers[2]; +static bool board=true, native=true, optin=true, baduuid; +static int nodes=1, nt=20*0x7c, ng=8, nr=64; +static u8 table[20*0x7c], group[8], ranges[64]; +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define for_each_compatible_node(np,t,c) for(int it=0;itavailable; } +static struct device_node *of_parse_phandle(struct device_node *p,const char *k,int i) { (void)k;(void)i;return p->report; } +static int of_property_read_string(struct device_node *p,const char *k,const char **s) { + (void)p;(void)k;*s=baduuid?"bad":"EFD60284-7C58-33A9-8522-CB3366AF6040";return 0; +} +static const void *of_get_property(struct device_node *p,const char *k,int *len) { + (void)p; + if (!strcmp(k,"asahi,j613-25g83-dcpext")) { *len=2;return optin?"1":NULL; } + if (!strcmp(k,"apple,tunable-soc-device")) { *len=nt;return table; } + if (!strcmp(k,"apple,tunable-soc-device-ps-group")) { *len=ng;return group; } + if (!strcmp(k,"apple,tunable-ptd-range")) { *len=nr;return ranges; } + return NULL; +} +''' +main = r''' +static void reset(void) { + board=native=optin=true;baduuid=false;nodes=1;nt=sizeof(table);ng=8;nr=64; + memset(table,0,sizeof(table));memset(group,0,sizeof(group));memset(ranges,0,sizeof(ranges)); + for(unsigned i=0;i<20;i++) put32(100+i,table+i*0x7c); + put32(8,table+11*0x7c);put32(9,table+12*0x7c);put32(18,table+19*0x7c); + put32(2,table+11*0x7c+8);put32(2,table+12*0x7c+8); + memcpy(table+11*0x7c+0x74,"DISP",5);memcpy(table+12*0x7c+0x74,"DISPEXT",8); + memcpy(table+19*0x7c+0x74,"ANS",4);put32(0x3fff0,group); + put32(10,ranges);put32(256,ranges+4);put32(8,ranges+8); + put32(11,ranges+32);put32(264,ranges+36);put32(8,ranges+40); + for(unsigned i=0;i<2;i++) { suppliers[i].available=true;suppliers[i].report=&report; } +} +static int admit(void) { struct device d={.of_node=&report};struct apple_pmp_report r={.dev=&d};return apple_pmp_native_records(&r); } +int main(void) { + reset();assert(!admit()); + reset();optin=false;board=false;assert(!admit()); /* Off leaves the old path intact. */ + reset();native=false;assert(!admit()); + reset();board=false;assert(admit()); + reset();nodes=0;assert(admit()); + reset();nodes=2;assert(admit()); + reset();baduuid=true;assert(admit()); + reset();suppliers[0].available=false;assert(admit()); + reset();nt--;assert(admit()); + reset();ng--;assert(admit()); + reset();nr--;assert(admit()); + reset();group[0]^=1;assert(admit()); + for(unsigned i=0;i<20;i++) { + if(i==11 || i==12 || i==19) continue; + reset();put32(8,table+i*0x7c);assert(admit()); + } + const unsigned positions[]={11,12,19}; + for(unsigned i=0;i<3;i++) { + reset();table[positions[i]*0x7c+0x74]^=1;assert(admit()); + reset();table[positions[i]*0x7c+8]^=2;assert(admit()); + } + reset();put32(257,ranges+4);assert(admit()); + puts("PASS native26 PTD personality, unique ordinals, acknowledgment and range refusals"); + return 0; +} +''' +with tempfile.TemporaryDirectory() as tmp: + p = Path(tmp) / 'native.c' + p.write_text(pre + validation + body + main) + binary = p.with_suffix('') + subprocess.run([os.environ.get('CC', 'cc'), '-std=gnu11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-function', '-O2', str(p), '-o', str(binary)], check=True) + subprocess.run([str(binary)], check=True) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index d7d079a9673a64..890293d825aca5 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -103,6 +103,8 @@ struct apple_pmp_report { struct list_head list; void *debugfs; struct apple_pmp_start *start; + struct device_node *native_pmp; + u64 native_claimed; }; struct completion { bool done; @@ -143,6 +145,7 @@ struct apple_pmp_report_entry { u32 id; bool no_ack, removed; bool sleep_release, released; + struct apple_pmp_report *native_owner; }; #define genpd_to_apple_pmp_report_entry(p) \ container_of(p, struct apple_pmp_report_entry, genpd) @@ -584,6 +587,13 @@ static const struct apple_pmp_start_ops apple_pmp_start_hw_ops = { hw_check, hw_bound }; static const struct apple_pmp_start_timing apple_pmp_start_timing = { 2, 0, 2, 2 }; +/* Native26 table admission is exercised by its separate actual-body fixture. */ +static int apple_pmp_native_records(struct apple_pmp_report *rep) +{ + assert(!rep->native_pmp); + return 0; +} + #include "production.h" static const struct apple_pmp_report_offsets * of_device_get_match_data(struct device *d) diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index 8757ee04cde1d6..cfbedd5bc0aecf 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -33,7 +33,7 @@ def main(): entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node)) assert len(entries) == 4 entry_data = ',\n'.join('{%d, %d, %d, %d}' % e for e in entries) - names = ['static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', + names = ['static int apple_pmp_report_record(', 'static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', 'static int apple_pmp_start_describe(', 'static void apple_pmp_start_finish(', 'static bool apple_pmp_start_stopping(', 'static void apple_pmp_start_work(', 'static void apple_pmp_start_drain(', 'static int apple_pmp_start_init(', diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index 1b51602b132cec..cc9985e3ae4ec1 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2335,6 +2335,102 @@ "tools/asahi/test-m3-power.py" ], "rust": "1.85.0" + }, + { + "name": "tools/asahi/dcpext26-wire-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-wire-tests.py" + ], + "note": "Production native26 external encoder against full byte fixtures and malformed reply/geometry cases; no firmware execution." + }, + { + "name": "tools/asahi/pmp-native26-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/pmp-native26-tests.py" + ], + "note": "Actual native26 report admission on synthetic board/OF/table inputs; no MMIO or firmware execution." + }, + { + "name": "tools/asahi/dcpext26-profile-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-profile-tests.py" + ], + "note": "Actual native26 callback helpers, startup peer and causal readiness; no hardware qualification." + }, + { + "name": "tools/asahi/dcpext26-lifecycle-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-lifecycle-tests.py" + ], + "note": "Actual retained-stop and control bodies with fault peer, host-drain and epoch budget fixtures." + }, + { + "name": "tools/asahi/dcpext26-edid-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-edid-tests.py" + ], + "note": "Actual native AV identity/generation/owner predicates and EDID wire header, length and checksums." + }, + { + "name": "tools/asahi/dcpext26-admission-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-admission-tests.py" + ], + "note": "Actual native identity and powered RUN/locked-DART admission predicates with bit and malformed-profile controls." + }, + { + "name": "tools/asahi/dcpext26-present-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-present-tests.py" + ], + "note": "Actual primary DRM callbacks: native start/submit rejection is terminal; accepted early D590 retains original receive time; classic behavior stays unchanged." + }, + { + "name": "tools/asahi/dcpext26-changeset-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-changeset-tests.py" + ], + "note": "Actual native property builder plus Linux OF update/remove/inversion bodies: absent/existing/equal values and rollback; ASan/UBSan allocation and queue failures." + }, + { + "name": "tools/asahi/dcpext26-startup-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-startup-tests.py" + ] + }, + { + "name": "tools/asahi/dcpext26-modeset-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-modeset-tests.py" + ] + }, + { + "name": "tools/asahi/dcpext26-parity-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-parity-tests.py" + ] } ], "helpers": { @@ -2343,7 +2439,7 @@ "tools/testing/selftests/drivers/gpu/asahi-g15/pmp-bridge.c": "compiled and executed by the registered asahi-g15/run.py maintained PMP fixture", "tools/asahi/t6030-pmp-graph-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", "tools/asahi/t6030-display-admission-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", - "tools/asahi/m3-uapi-tests.rs": "compiled and executed by the registered tools/asahi/test-m3-uapi.py paired-header suite" + "tools/asahi/m3-uapi-tests.rs": "compiled and run by the registered tools/asahi/test-m3-uapi.py C8 UAPI fixture" }, "excluded": { "tools/aurora-sep/tests/coordinator/runtime-check.sh": "Needs /dev/sep-bio, a running kernel booted with trusted.source=applesep, the hw_random sysfs interface and fprintd. Coordinator payload for an approved device.", diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index 4097d7db7d86ae..7a4ba475b75b4c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -61,10 +61,68 @@ def tipd_pm_header(root): '#define clamp(val, lo, hi) ({ __auto_type _v = (val); ' '__typeof__(_v) _lo = (lo), _hi = (hi); _v < _lo ? _lo : _v > _hi ? _hi : _v; })') +def classic_unit(unit): + # This suite exercises established profiles. Native26 has separate actual-body + # fixtures; model its optional branch and storage without changing assertions. + if 'dcp_is_ext26(' in unit and not re.search(r'^static [^\n]+ dcp_is_ext26\(', unit, re.M): + unit = '#define dcp_is_ext26(d) 0\n' + unit + # Native-only diagnostic/OPEN branches still need declarations when parsed. + for token, value in (('SYSTEM_ENDPOINT', '0x20'), ('DISP0_ENDPOINT', '0x23')): + if token in unit and not re.search(r'#define ' + token + r'\s', unit): + unit = '#define ' + token + ' ' + value + '\n' + unit + if 'dptx_native_controllers_ready(' in unit: + unit = '#define dptx_native_controllers_ready(d) 0\n' + unit + if 'dcp_is_retained_external(' in unit and not re.search(r'^(?:static )?bool dcp_is_retained_external\(', unit, re.M): + predicate = 'dcp_is_external_h17p(d)' if 'dcp_is_external_h17p(' in unit else '((d)->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL)' + unit = '#define dcp_is_retained_external(d) ' + predicate + '\n' + unit + if 'ext26_' in unit and 'struct apple_dcp {' in unit: + mock = block(unit, 'struct apple_dcp {') + fields = [] + for name in ('ext26_pm_preparing', 'ext26_stopping', 'ext26_normal', 'ext26_blocked', 'external_stopped', 'crashed', 'ext26_services_open', 'ext26_client_open', 'ext26_edid_tried'): + if name not in mock: + fields.append('bool ' + name + ';') + if 'ext26_edid_generation' not in mock: + fields.append('u64 ext26_edid_generation;') + if 'ext26_aux' not in mock: + fields.append('struct apple_dcp_afkep *ext26_aux[3];') + if 'ext26_blank_work' not in mock: + fields.append('int ext26_blank_work;') + for name in ('systemep', 'ibootep', 'dptx_controller_ep', 'dptxep', 'dcpavservep'): + if 'dcp->' + name in unit and not re.search(r'\b' + name + r'\b', mock): + fields.append('struct apple_dcp_afkep *' + name + ';') + if 'dcp->rtk' in unit and not re.search(r'\brtk\b', mock): + fields.append('void *rtk;') + unit = unit.replace('struct apple_dcp {', 'struct apple_dcp {\n' + '\n'.join(fields), 1) + if 'struct apple_dcp_afkep *endpoints' in unit and 'struct apple_dcp_afkep {' not in unit: + unit = 'struct apple_dcp_afkep { void *wq; };\n' + unit + for name, expression in (('afk_host_idle', '1'), ('iomfb_ext26_retain_epoch', '0'), + ('apple_rtkit_flush_rx', '((void)0)'), ('flush_workqueue', '((void)0)'), + ('flush_work', '((void)0)'), ('queue_work', '0'), + ('dcp_modes_begin_attachment', '((void)0)')): + if name + '(' in unit and not re.search(r'^(?:static )?[^\n;]+\b' + name + r'\([^;]*\)\s*\{', unit, re.M): + unit = '#define ' + name + '(...) ' + expression + '\n' + unit + if 'struct apple_dcp_afkep *endpoints' in unit: + unit = '#ifndef ARRAY_SIZE\n#define ARRAY_SIZE(a) (sizeof(a)/sizeof(*(a)))\n#endif\n#ifndef IS_ERR_OR_NULL\n#define IS_ERR_OR_NULL(p) (!(p))\n#endif\n' + unit + if 'WRITE_ONCE(' in unit and '#define WRITE_ONCE' not in unit: + unit = '#define WRITE_ONCE(x,v) ((x)=(v))\n' + unit + if 'DPAVSERV_ENDPOINT' in unit and not re.search(r'#define DPAVSERV_ENDPOINT\s', unit): + unit = '#define DPAVSERV_ENDPOINT 0x28\n' + unit + if 'dcp_direct_crossbar_link(' in unit and not re.search(r'^(?:static )?[^\n;]+\bdcp_direct_crossbar_link\([^;]*\)\s*\{', unit, re.M): + unit = '#define dcp_direct_crossbar_link(d,on) 0\n' + unit + if 'ext26_prop_start(' in unit: + unit = '#define ext26_prop_start(d,n) 0\n' + unit + if 'dcp->chunks.received' in unit: + unit = unit.replace('void *data;\n size_t length;', 'void *data;\n size_t received; bool contiguous; size_t length;') + if 'ext26_command_bytes' in unit: + unit = unit.replace('struct apple_dcp_afkep {', 'struct apple_dcp_afkep { size_t ext26_command_bytes; unsigned ext26_command_count;', 1) + unit = '#ifndef ALIGN\n#define ALIGN(x,a) (((x)+(a)-1)&~((a)-1))\n#endif\n#ifndef SZ_16K\n#define SZ_16K 16384UL\n#endif\n#ifndef SZ_16M\n#define SZ_16M (16UL<<20)\n#endif\n' + unit + return unit + def run_unit(name, unit, output, cases=((),), includes=(), extra=()): work = output / name work.mkdir(parents=True, exist_ok=True) source = work / 'host.c' + unit = classic_unit(unit) source.write_text(unit) command = shlex.split(os.environ.get('HOSTCC', 'clang')) + [ '-std=gnu11', '-Wall', '-Werror', '-O1', '-g', diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py index 1cee66408389df..d62d5d529a223d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py @@ -848,9 +848,9 @@ def function(text, signature): struct { struct mutex lock; bool stopped; void *owner; } iomfb; }; struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; -struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; +struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); void (*recv_message_timed)(void *,u8,u64,u64); }; struct apple_rtkit { bool shutting_down, sync_syslog_ack; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; -struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; +struct apple_rtkit_rx_work { u64 received_ns; struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; #define current ((void *)1) static pthread_mutex_t reply_lock=PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t reply_ready=PTHREAD_COND_INITIALIZER; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 96fd0b5dcea014..8ed4875f713f03 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1340,7 +1340,8 @@ def function(source, marker): # The T8122 dcpext wrapper moved the pipe's probe into dcp_platform_probe_pipe(). probe = function(dcp, 'static int dcp_platform_probe_pipe(' if 'static int dcp_platform_probe_pipe(' in dcp else 'static int dcp_platform_probe(') - publish = probe[probe.rindex('\tif (dcp->hw.iomfb_method_profile ==\n\t DCP_IOMFB_METHODS_H17P_EXTERNAL) {'):] + publish_marker = '\tif (dcp_is_retained_external(dcp)) {' if 'dcp_is_retained_external(dcp)' in probe else '\tif (dcp->hw.iomfb_method_profile ==\n\t DCP_IOMFB_METHODS_H17P_EXTERNAL) {' + publish = probe[probe.rindex(publish_marker):] # Which processor must come up before the boot framebuffer is taken over. header = root / 'drivers/gpu/drm/apple/apple_boot_display.h' boot_display = '' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py index 0a26ac0e584f13..75bd0c64578520 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py @@ -7,6 +7,7 @@ """ from pathlib import Path import json, hashlib, subprocess +from common import classic_unit def check(root, output): import edid @@ -264,6 +265,7 @@ def extract(text,signature): includes=[root/'drivers/gpu/drm/apple',root/'tools/testing/selftests/drivers/gpu/apple-afkv2/host-include'] results=[] def run(name,text,cases): + text=classic_unit(text) area=out/name;area.mkdir(exist_ok=True);(area/'host.c').write_text(text) cmd=['clang','-std=gnu11','-Wall','-Werror','-Wno-unused-function','-O1','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer']+['-I'+str(p) for p in includes]+[str(area/'host.c'),'-o',str(area/'host')] (area/'command.json').write_text(json.dumps(cmd,indent=2)+'\n') From 9eed24860e5341a19fa85fa7b1d6d74a9f02e80e Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 13:55:11 -0500 Subject: [PATCH 14/15] selftests: apple: refresh display and PMP host boundaries Signed-off-by: Satya Benson --- tools/asahi/t6030-pmp-graph-tests.c | 1 + tools/asahi/t6030-pmp-graph-tests.py | 2 +- tools/asahi/t6030-pmp-supplier-tests.c | 9 +++++++++ .../drivers/gpu/apple-dcp/plane-layout.py | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index 890293d825aca5..93b21cc493c92b 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -23,6 +23,7 @@ typedef uint8_t u8; #define PMP_REPORT_READY 1 #define GFP_KERNEL 0 #define GENPD_FLAG_ACTIVE_WAKEUP 1 +#define GENPD_FLAG_ALWAYS_ON 2 #define MAX_PHANDLE_ARGS 4 #define EPROBE_DEFER 517 #define SZ_4K 4096 diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index cfbedd5bc0aecf..1dfb03f10c4de5 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -84,7 +84,7 @@ def with_t8122(text): ('no-managed-owner', '.starts_pmp = IS_ENABLED(CONFIG_APPLE_T6030_GPU_PMP_LINK)', '.starts_pmp = false'), ('no-managed-probe', 'ret = apple_pmp_start_init(rep);', 'ret = 0; (void)apple_pmp_start_init;'), ('no-child-seeding', 'apple_pmp_report_entry_set_state(&ent->genpd, true);', '(void)0;'), - ('no-storage-policy', 'ent->no_ack = of_property_read_bool(node, "apple,no-ack");', 'ent->no_ack = false;'), + ('no-storage-policy', 'ent->no_ack = of_property_read_bool(node, "apple,no-ack");\n\t}', 'ent->no_ack = false;\n\t}'), ('external-startup-ack', '!of_property_read_bool(child, "apple,defer-startup-ack")', 'true'), ('ignore-seeded-state', 'status || request != st->seed', 'status'), ('ignore-halted-state', 'status || request != st->seed', 'request != st->seed'), diff --git a/tools/asahi/t6030-pmp-supplier-tests.c b/tools/asahi/t6030-pmp-supplier-tests.c index 40e6be5bf24c57..5dd0345029c47f 100644 --- a/tools/asahi/t6030-pmp-supplier-tests.c +++ b/tools/asahi/t6030-pmp-supplier-tests.c @@ -47,6 +47,7 @@ struct apple_pmp_report_offsets { const char *name, *fallback, *pwrstate; }; struct apple_pmp_report { + struct device_node *native_pmp; void *base; const struct apple_pmp_report_offsets *offsets; struct device *dev; @@ -84,6 +85,14 @@ static void rcu_read_unlock(void) { assert(rcu_depth-- == 1); } +static int apple_pmp_report_record(struct device_node *pmp, + struct device_node *entry, u32 *id, bool *record) +{ + (void)pmp; + *id = entry->reg; + *record = false; + return 0; +} static int apple_pmp_report_bit(struct apple_pmp_start *st, struct device_node *entry, u32 *id, bool *record) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py b/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py index e7b12e5bdbb59f..c00431b1e9b53b 100755 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py @@ -110,15 +110,26 @@ def unit(root, ref=None, mutation=None, callback_ref=None): struct drm_rect { int x1,y1,x2,y2; }; enum drm_color_range { DRM_COLOR_YCBCR_LIMITED_RANGE, DRM_COLOR_YCBCR_FULL_RANGE }; enum drm_color_encoding { DRM_COLOR_YCBCR_BT601, DRM_COLOR_YCBCR_BT709, DRM_COLOR_YCBCR_BT2020 }; -struct drm_crtc_state { struct { int hdisplay,vdisplay; } mode; }; -struct drm_plane_state { void *crtc; bool visible; struct drm_framebuffer *fb; struct drm_rect src,dst; enum drm_color_range color_range; enum drm_color_encoding color_encoding; }; +struct drm_crtc_state { struct { int hdisplay,vdisplay; } mode; bool vrr_enabled; }; +struct drm_plane_state { u32 rotation,src_x,src_y,src_w,src_h; int crtc_x,crtc_y,crtc_w,crtc_h; void *crtc; bool visible; struct drm_framebuffer *fb; struct drm_rect src,dst; enum drm_color_range color_range; enum drm_color_encoding color_encoding; }; struct drm_plane; struct drm_plane_funcs { bool (*format_mod_supported)(struct drm_plane *,u32,u64); }; -struct drm_plane { struct { u32 id; } base;struct drm_plane_state *state;u32 *format_types;unsigned int format_count;u64 *modifiers;unsigned int modifier_count;const struct drm_plane_funcs *funcs; }; +struct drm_plane { int type; struct { u32 id; } base;struct drm_plane_state *state;u32 *format_types;unsigned int format_count;u64 *modifiers;unsigned int modifier_count;const struct drm_plane_funcs *funcs; }; struct drm_atomic_commit { struct { void *dev; } *dev; }; ''' prefix += block(plane_header, 'struct apple_plane_state {') + ';\n' prefix += r''' +/* Layout controls use the legacy DCP; ext26 admission has its own suite. */ +#define DRM_PLANE_TYPE_PRIMARY 1 +#define DRM_MODE_ROTATE_0 1 +struct platform_device { int unused; }; +struct apple_dcp { int unused; }; +struct apple_crtc { struct platform_device *dcp; }; +static struct apple_crtc host_crtc; +static struct apple_crtc *to_apple_crtc(void *crtc) { return &host_crtc; } +static struct apple_dcp *platform_get_drvdata(struct platform_device *pdev) { return NULL; } +static bool dcp_is_ext26(struct apple_dcp *dcp) { return false; } +static bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp,struct drm_framebuffer *fb) { return true; } #define to_apple_plane_state(p) container_of(p,struct apple_plane_state,base) static struct drm_crtc_state crtc_state = { .mode = { 3840,2160 } }; static struct drm_plane_state *drm_atomic_get_new_plane_state(struct drm_atomic_commit *c,struct drm_plane *p) { return p->state; } From 953df238efbb0c2b028d49b7aa2da5788cbba180 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 13:56:23 -0500 Subject: [PATCH 15/15] selftests: apple: keep child-seeding mutant compilable Signed-off-by: Satya Benson --- tools/asahi/t6030-pmp-graph-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index 1dfb03f10c4de5..61737534128f9b 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -83,7 +83,7 @@ def with_t8122(text): controls = [ ('no-managed-owner', '.starts_pmp = IS_ENABLED(CONFIG_APPLE_T6030_GPU_PMP_LINK)', '.starts_pmp = false'), ('no-managed-probe', 'ret = apple_pmp_start_init(rep);', 'ret = 0; (void)apple_pmp_start_init;'), - ('no-child-seeding', 'apple_pmp_report_entry_set_state(&ent->genpd, true);', '(void)0;'), + ('no-child-seeding', 'apple_pmp_report_entry_set_state(&ent->genpd, true);', '0;'), ('no-storage-policy', 'ent->no_ack = of_property_read_bool(node, "apple,no-ack");\n\t}', 'ent->no_ack = false;\n\t}'), ('external-startup-ack', '!of_property_read_bool(child, "apple,defer-startup-ack")', 'true'), ('ignore-seeded-state', 'status || request != st->seed', 'status'),