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/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/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_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_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_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_compute.rs b/drivers/gpu/drm/asahi/m3/m3_compute.rs index 34aa0c0d2cd3fb..b6e9444ed54499 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) } @@ -304,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)?; @@ -319,26 +367,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.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} - 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 +414,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_compute_layout.rs b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs index 74bed858b71e26..ace58e38f38a6d 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,29 @@ 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> { + 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 + || (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 +100,157 @@ 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); + 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()); 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]); + } + #[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 c4a44c4b4f3b0e..02b847e210dc9b 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -49,6 +49,9 @@ impl Records { 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, @@ -65,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 { @@ -140,7 +154,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, 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, request_check_at:None,request_writes:0,request_observed:0 }) @@ -153,6 +168,25 @@ 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 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)?; @@ -161,6 +195,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(());} @@ -192,6 +228,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)?; @@ -202,14 +246,38 @@ 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[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)?; @@ -229,6 +297,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 { @@ -261,6 +330,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. @@ -268,6 +340,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)?; @@ -278,12 +357,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(); @@ -297,6 +378,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)?; @@ -312,7 +405,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; @@ -321,8 +414,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; @@ -343,10 +436,40 @@ 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(|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]; + 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 +515,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_device.rs b/drivers/gpu/drm/asahi/m3/m3_device.rs index c43017a8933c0f..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 { @@ -176,6 +177,11 @@ 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. + 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 bcb9fd32c7d255..922d76ba2055bc 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>>, @@ -112,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(); @@ -125,6 +128,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 +164,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 +212,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 +223,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 +241,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) @@ -252,7 +260,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_firmware.rs b/drivers/gpu/drm/asahi/m3/m3_firmware.rs index 0d2835174c6775..94a2d3384d08aa 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), }]; +/// 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, + 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 segment and loader-mutable bounds. +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 } @@ -178,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>, @@ -191,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); @@ -215,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)?; @@ -393,4 +519,75 @@ 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])); + } + + #[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_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_init_26.rs b/drivers/gpu/drm/asahi/m3/m3_init_26.rs new file mode 100644 index 00000000000000..a86432b2bbe2b5 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_init_26.rs @@ -0,0 +1,293 @@ +// 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, 39 | 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 < 0xffff_fc20_0000_0000 + || 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]); + } + #[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_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..a17bc0ee399c34 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs @@ -0,0 +1,294 @@ +// 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 writable(self) -> bool { + self.writable + } + + 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); + } + 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, + 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::*; + + // Resource owners exercise subpage mappings and split memory-controller banks. + 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]); + } + } + #[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_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_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_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/m3_profile.rs b/drivers/gpu/drm/asahi/m3/m3_profile.rs new file mode 100644 index 00000000000000..04b872ed9ea809 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_profile.rs @@ -0,0 +1,110 @@ +// 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, + } + } + + /// 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), + 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 + } +} + +#[cfg(test)] +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] { + 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); + } + #[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_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 1a366d7f4b3407..3b1f3d6d085356 100644 --- a/drivers/gpu/drm/asahi/m3/m3_runtime.rs +++ b/drivers/gpu/drm/asahi/m3/m3_runtime.rs @@ -389,8 +389,8 @@ 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::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::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.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(()) @@ -469,6 +470,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 +539,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);}; @@ -560,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()?)); }, @@ -1059,7 +1064,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 +1108,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 { @@ -1214,6 +1220,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 +1254,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, @@ -1312,13 +1375,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 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_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/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..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, @@ -177,11 +178,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,14 +191,14 @@ 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 { 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) { @@ -211,13 +213,15 @@ 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; } 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); } @@ -225,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}, @@ -238,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/mod.rs b/drivers/gpu/drm/asahi/m3/mod.rs index 1d10657c02f662..d885da1a8647ca 100644 --- a/drivers/gpu/drm/asahi/m3/mod.rs +++ b/drivers/gpu/drm/asahi/m3/mod.rs @@ -198,3 +198,12 @@ mod t8122_admission; pub(crate) mod m3_soc; 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/drivers/gpu/drm/asahi/m3/t8122_admission.rs b/drivers/gpu/drm/asahi/m3/t8122_admission.rs index 7505065810acc3..ffa04859c6b936 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_admission.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_admission.rs @@ -1,6 +1,8 @@ // 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; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) struct Identity { @@ -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 @@ -40,6 +53,41 @@ pub(crate) fn validate_identity( }) } +/// Same mutually exclusive board identities and exact string opt-in as m1n1/DCP. +/// J615 shares the 25G83 GPU/ADT profile, but has no hardware qualification here. +pub(crate) fn extended_board(j613: bool, j615: bool, optin: Option<&[u8]>) -> bool { + (j613 && !j615) || (!j613 && j615 && optin == Some(b"1\0")) +} + +/// Firmware-interface selection is exact and does not change physical GPU encodings. +pub(crate) fn profile_for(compat:&[u32],air:bool)->Option { + match compat { + [14,8,3]=>Some(Profile::Legacy), + [26,6,2] if air=>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 air = { + let j613 = super::m3_board::board_is(b"apple,j613"); + let j615 = super::m3_board::board_is(b"apple,j615"); + let optin = kernel::of::chosen().and_then(|chosen| { + chosen.get_property::>(c_str!("asahi,j615-25g83-experimental")).ok() + }); + extended_board(j613, j615, optin.as_deref()) + }; + #[cfg(not(CONFIG_DRM_ASAHI_M3_T8122))] + let air = false; + profile_for(&compat, air).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 @@ -94,6 +142,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, loaded_profile(pdev)?) +} + +#[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 +202,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 +221,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] = [ @@ -255,3 +335,27 @@ 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 j615_requires_exact_optin_and_unambiguous_board() { + for optin in [None, Some(&b""[..]), Some(&b"1"[..]), Some(&b"0\0"[..]), + Some(&b"1\0extra"[..]), Some(&[0, 0, 0, 1][..]), Some(&b"1\0"[..])] { + assert!(extended_board(true, false, optin)); + assert!(!extended_board(false, false, optin)); + assert!(!extended_board(true, true, optin)); + assert_eq!(extended_board(false, true, optin), optin == Some(&b"1\0"[..])); + } + } + #[test] fn extended_admission_requires_exact_firmware_and_air() { + 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/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-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..713c82f42250fa 100644 --- a/tools/asahi/m3-runtime-tests.rs +++ b/tools/asahi/m3-runtime-tests.rs @@ -15,3 +15,70 @@ 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; + +#[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; + +#[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; + +#[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; 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-j613-dcp-power.py b/tools/asahi/test-j613-dcp-power.py new file mode 100644 index 00000000000000..3bc1a3379c253e --- /dev/null +++ b/tools/asahi/test-j613-dcp-power.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the production J613 PMGR predicate and test PMGR fields and unrelated bits.""" +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) { + /* 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 = unrelated[i] | (actual << 4) | target | (automatic << 28); + bool expected = actual == 15 || (target == 15 && automatic); + 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: 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 (["-O0"], ["-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/asahi/test-m3-mailbox.py b/tools/asahi/test-m3-mailbox.py new file mode 100644 index 00000000000000..167b0566b2e5dd --- /dev/null +++ b/tools/asahi/test-m3-mailbox.py @@ -0,0 +1,101 @@ +#!/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)); } + // 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() { +''' + '\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/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/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 a1b93d695dfd6e..1b51602b132cec 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -1,2319 +1,2373 @@ { - "comment": [ - "Hermetic host test suites, run by host-tests.py from the top of the tree.", - "{output} is a fresh path that does not exist yet; its parent does.", - "expect: pass (exit 0, the default) or fail (non-zero; mutation checks that report a caught mutant that way).", - "known_failure: runs, is reported, and does not fail the job; remove it once the suite passes.", - "commits: history the suite reads; host-tests.py --fetch-commits fetches it into a shallow checkout.", - "Every test-like file under the discovery roots is listed in suites or excluded (tests/test_host_tests.py checks).", - "helpers: files that look like suites but are run by a listed suite.", - "serial: the suite measures time; it runs alone, after the parallel batch.", - "group: the CI job that runs the suite; aurora-sep-slow holds the longest aurora-sep suites so they run beside the rest." - ], - "apt": [ - "bison", - "bubblewrap", - "clang", - "dbus", - "device-tree-compiler", - "flex", - "libarchive-tools", - "libegl-dev", - "libgles-dev", - "libnotify-bin", - "lld", - "llvm", - "make", - "makepkg", - "python3-gi", - "zstd" - ], - "discovery": [ - "tools/asahi", - "tools/asahi-m3", - "tools/aurora-*", - "tools/testing/apple", - "tools/testing/selftests/drivers/apple", - "tools/testing/selftests/drivers/gpu/apple-*", - "tools/testing/selftests/drivers/gpu/asahi-g15" - ], - "suites": [ - { - "name": "tools/asahi/apple-dart-fw-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/apple-dart-fw-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/apple-dart-range-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/apple-dart-range-tests.py" - ], - "known_failure": "its mocks predate the current apple-dart.c (no u32, fw_mirror/fw_handoff, apple_dart_fw_invalidate; apple_dart_hw_sync_locked takes three arguments now)" - }, - { - "name": "tools/asahi/avd-codec-tail-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-codec-tail-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py --mutation round-up", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}", - "--mutation", - "round-up" - ], - "expect": "fail", - "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py --mutation cpu-unaligned", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}", - "--mutation", - "cpu-unaligned" - ], - "expect": "fail", - "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" - }, - { - "name": "tools/asahi/bcm4377-id-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-id-tests.py", - "--out", - "{output}", - "--old-ref", - "87ddae1397f6f70817cb57df76477a86bdf96424" - ], - "commits": [ - "87ddae1397f6f70817cb57df76477a86bdf96424" - ] - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation epoch", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "epoch" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation close", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "close" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation producer-fence", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "producer-fence" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation active-fence", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "active-fence" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation board", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "board" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --old-ref 36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--old-ref", - "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" - ], - "expect": "pass", - "note": "self-checking: original driver (87ddae1397f6^) must abort cases 0,1,2,28", - "commits": [ - "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" - ] - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation short-ie", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "short-ie" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation header-length", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "header-length" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation skb-capacity", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "skb-capacity" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation cmplt-header", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "cmplt-header" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --old-association f58ae50192550e6fc67c956f361f525881a1f969", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--old-association", - "f58ae50192550e6fc67c956f361f525881a1f969" - ], - "expect": "fail", - "note": "inverted; old getter from ecd1ea4d0f06^", - "commits": [ - "f58ae50192550e6fc67c956f361f525881a1f969" - ] - }, - { - "name": "tools/asahi/brcmf-bss-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-bss-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmfmac-scan-timeout-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmfmac-scan-timeout-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmfmac-scan-timeout-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmfmac-scan-timeout-tests.py", - "--output", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; cfg80211.c from f58ae5019255^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 661c0a5740e8f36816f7e79a784c36c026097368:drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "661c0a5740e8f36816f7e79a784c36c026097368" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation narrow-stream", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "narrow-stream" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation ignore-aperture", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "ignore-aperture" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation revision-wide", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "revision-wide" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --old-dma 654386a164a1e93063f879491f10bebfc1439291", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--old-dma", - "654386a164a1e93063f879491f10bebfc1439291" - ], - "expect": "fail", - "note": "inverted; probe from dbdeefb63947^", - "commits": [ - "654386a164a1e93063f879491f10bebfc1439291" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --firmware-revision b0de24a8c59735f21023c93592dd9a2eb9e155a4", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--firmware-revision", - "b0de24a8c59735f21023c93592dd9a2eb9e155a4" - ], - "expect": "fail", - "note": "inverted; firmware table from 2d17f269503c^", - "commits": [ - "b0de24a8c59735f21023c93592dd9a2eb9e155a4" - ] - }, - { - "name": "tools/asahi/brcmf-roam-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-roam-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/compute-preempt-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/compute-preempt-tests.py", - "--out", - "{output}" - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/compute-preempt-tests.py --old-ref f885ea0de2cd638bda1e0d3d38a901d927a503f5", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/compute-preempt-tests.py", - "--out", - "{output}", - "--old-ref", - "f885ea0de2cd638bda1e0d3d38a901d927a503f5" - ], - "rust": "1.85.0", - "expect": "pass", - "note": "self-checking; policy from c6f4ea502bdd^", - "commits": [ - "f885ea0de2cd638bda1e0d3d38a901d927a503f5" - ] - }, - { - "name": "tools/asahi/dcp14-backlight-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-backlight-tests.py" - ] - }, - { - "name": "tools/asahi/dcp14-backlight-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-backlight-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking (-DOLD); iomfb_v14_7.c from 2e5a8e777ada^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show e19eb212f57ec32db9026666c89dd610062e9f24:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "e19eb212f57ec32db9026666c89dd610062e9f24" - ] - }, - { - "name": "tools/asahi/dcp14-panel-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-panel-tests.py" - ] - }, - { - "name": "tools/asahi/dcp14-panel-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-panel-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; iomfb_v14_7.c from 7d5a612eceaa^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show a5713d126ea098fd3b6b833fc4022762540aac6c:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "a5713d126ea098fd3b6b833fc4022762540aac6c" - ] - }, - { - "name": "tools/asahi/devcoredump-init-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/devcoredump-init-tests.py" - ] - }, - { - "name": "tools/asahi/devcoredump-init-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/devcoredump-init-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking (-DOLD); devcoredump.c from ebd3dc459ac1^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show fb7cca304cb381dc589fadbed479c6f3b36e5e23:drivers/base/devcoredump.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "fb7cca304cb381dc589fadbed479c6f3b36e5e23" - ] - }, - { - "name": "tools/asahi/display26-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/display26-tests.py" - ] - }, - { - "name": "tools/asahi/dma-heap-fd-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dma-heap-fd-tests.py", - "--out", - "{output}", - "--old-ref", - "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" - ], - "commits": [ - "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" - ] - }, - { - "name": "tools/asahi/mt7932-association-events-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-association-events-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/mt7932-association-events-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-association-events-tests.py", - "--out", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; link.c from c980f7435d50^ (the fix). The test commit itself (f885ea0de2cd) is test-only, so its parent is NOT a valid old source.", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 818bde483e1618783005ac76e1c58057a4bb4cfa:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "818bde483e1618783005ac76e1c58057a4bb4cfa" - ] - }, - { - "name": "tools/asahi/mt7932-connect-result-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-connect-result-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/mt7932-connect-result-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-connect-result-tests.py", - "--output", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; link.c from 6c3ee18a0725^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 7db4eadeeba6512726fc527cb624ae99742de607:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "7db4eadeeba6512726fc527cb624ae99742de607" - ] - }, - { - "name": "tools/asahi/mt7932-owe-retry-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-owe-retry-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}" - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py --mutation", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}", - "--mutation" - ], - "rust": "1.85.0", - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py --old", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}", - "--old" - ], - "rust": "1.85.0", - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation sep-syscore", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "sep-syscore" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation wake-irq", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "wake-irq" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation stale-policy", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "stale-policy" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --old-usb", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--old-usb" - ], - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py --old-regulatory", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--old-regulatory" - ], - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/pmp-j613-hwmon-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/pmp-j613-hwmon-tests.py" - ] - }, - { - "name": "tools/asahi/qspi-nvram-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/qspi-nvram-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate admission", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "admission" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate channel", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "channel" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate counter", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "counter" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate reset", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "reset" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --ref 0cee491386e3dff5a09b60d5703f6a7be71b0883", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--ref", - "0cee491386e3dff5a09b60d5703f6a7be71b0883" - ], - "expect": "fail", - "note": "inverted; rtkit.c from 1369d18b4ec7^", - "commits": [ - "0cee491386e3dff5a09b60d5703f6a7be71b0883" - ] - }, - { - "name": "tools/asahi/tbnet-refill-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant refill-count", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "refill-count" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant no-retry", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "no-retry" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant no-cancel", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "no-cancel" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant worker-allocation", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "worker-allocation" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --ref f7f5fc8e97cf5238c8669d9fca0d024b40174209", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--ref", - "f7f5fc8e97cf5238c8669d9fca0d024b40174209" - ], - "expect": "fail", - "note": "inverted; main.c from 71700a159448^", - "commits": [ - "f7f5fc8e97cf5238c8669d9fca0d024b40174209" - ] - }, - { - "name": "tools/asahi/tunnel-route-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tunnel-route-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/m3-batching-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/m3-batching-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/m3-runtime-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/m3-runtime-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/pmp-bootargs-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/pmp-bootargs-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/pmp-power-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/pmp-power-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi-m3/test_completion_progress.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test_completion_progress.py" - ] - }, - { - "name": "tools/asahi-m3/test_fault_snapshot.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test_fault_snapshot.py" - ] - }, - { - "name": "tools/asahi-m3/test-retirement-progress.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test-retirement-progress.py" - ] - }, - { - "name": "tools/asahi-m3/test-gem-object-size.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test-gem-object-size.py" - ], - "rust": "1.85.0" - }, - { - "name": "tools/aurora-sep/tests/test_adt_extract.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_adt_extract.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_air_gpu.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_air_gpu.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_desktop_fixes.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_desktop_fixes.py", - "-v" - ], - "commits": [ - "05d6db3921dec6a6667a7ec8b1b0c75f0f66bafe" - ] - }, - { - "name": "tools/aurora-sep/tests/test_diag_sensor.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_diag_sensor.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_download_resume.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_download_resume.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_esp_history.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_esp_history.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_extract_mesa_calibration.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_extract_mesa_calibration.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_frozen_dependencies.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_frozen_dependencies.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_host_entropy.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_host_entropy.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_host_state_check.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_host_state_check.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m1n1_rollback.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m1n1_rollback.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air_default.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air_default.py", - "-v" - ], - "commits": [ - "4f2a68f8b88a6b407f072356c248d22f076b401a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air_mesa.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air_mesa.py", - "-v" - ], - "commits": [ - "4f2a68f8b88a6b407f072356c248d22f076b401a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_boot_profile.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_boot_profile.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_cmdline.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_cmdline.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_flow.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_flow.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_gpu_check.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_gpu_check.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_handoff.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_handoff.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_kernel_only.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_kernel_only.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_max.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_max.py", - "-v" - ], - "commits": [ - "2ca6ff6e3480e11dfd51b720763c0fbd99fa015e", - "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc" - ], - "serial": true - }, - { - "name": "tools/aurora-sep/tests/test_m3_persistent.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_persistent.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_pro_mesa.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_pro_mesa.py", - "-v" - ], - "commits": [ - "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc", - "df0c433027e9eb283b14a56894efcc817c8a3cfb", - "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_pro_perf.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_pro_perf.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_report.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_report.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_stack_assembly.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_stack_assembly.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3max_kit.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3max_kit.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_neo_gpu.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_neo_gpu.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_packaging.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_packaging.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_pm_domains.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_pm_domains.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_ready_pam_hint.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_ready_pam_hint.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_refkey_seal_admission.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_refkey_seal_admission.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_release.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_release.py", - "-v" - ], - "commits": [ - "d14f756c9a65752ca532b93391f775387ba4f191", - "df0c433027e9eb283b14a56894efcc817c8a3cfb", - "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sbio_exchange.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sbio_exchange.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sensor_recheck.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sensor_recheck.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sensor_wake.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sensor_wake.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sep_ready.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sep_ready.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_store_shipped.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_store_shipped.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_t8112_sep_dma.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_t8112_sep_dma.py", - "-v" - ], - "setup": [ - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "allnoconfig" - ], - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "scripts_dtc" - ] - ], - "env": { - "DTC": "{output}/kbuild/scripts/dtc/dtc" - } - }, - { - "name": "tools/aurora-sep/tests/test_t8122_sep.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_t8122_sep.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_unsupported_soc.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_unsupported_soc.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_verify_bringup.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_verify_bringup.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_xart_locator.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_xart_locator.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (sh)", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (bash)", - "group": "tools", - "argv": [ - "bash", - "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", - "group": "tools", - "setup": [ - [ - "cc", - "-std=c11", - "-Wall", - "-Wextra", - "-Werror", - "-pedantic", - "-O2", - "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", - "-ldl", - "-o", - "{output}" - ] - ], - "argv": [ - "{output}" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh" - ] - }, - { - "name": "tools/aurora-display-notifier/tests/test_notifier.py", - "group": "tools", - "argv": [ + "comment": [ + "Hermetic host test suites, run by host-tests.py from the top of the tree.", + "{output} is a fresh path that does not exist yet; its parent does.", + "expect: pass (exit 0, the default) or fail (non-zero; mutation checks that report a caught mutant that way).", + "known_failure: runs, is reported, and does not fail the job; remove it once the suite passes.", + "commits: history the suite reads; host-tests.py --fetch-commits fetches it into a shallow checkout.", + "Every test-like file under the discovery roots is listed in suites or excluded (tests/test_host_tests.py checks).", + "helpers: files that look like suites but are run by a listed suite.", + "serial: the suite measures time; it runs alone, after the parallel batch.", + "group: the CI job that runs the suite; aurora-sep-slow holds the longest aurora-sep suites so they run beside the rest." + ], + "apt": [ + "bison", + "bubblewrap", + "clang", + "dbus", + "device-tree-compiler", + "flex", + "libarchive-tools", + "libegl-dev", + "libgles-dev", + "libnotify-bin", + "lld", + "llvm", "make", - "-C", - "tools/aurora-display-notifier/", - "test" - ] - }, - { - "name": "tools/aurora-display-notifier/tests/integration_dbus.py", - "group": "tools", - "argv": [ - "dbus-run-session", - "--", - "env", - "AURORA_TEST_PRIVATE_BUS=1", - "python3", - "tools/aurora-display-notifier/tests/integration_dbus.py" - ] - }, - { - "name": "tools/aurora-tb/pr14-regression.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-tb/pr14-regression.py", - "--negative-controls" - ] - }, - { - "name": "tools/aurora-tb/tbnet-lifecycle-test.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-tb/tbnet-lifecycle-test.py", - "--source", - ".", - "--output", - "{output}", - "--negative-controls" - ], - "known_failure": "its fixture predates the receive refill retry (1d25feb6bb33): no rx_refill_work member or cancel_delayed_work_sync mock" - }, - { - "name": "tools/aurora-wake/test-s2idle-rtc-regression.sh", - "group": "tools", - "argv": [ - "bash", - "tools/aurora-wake/test-s2idle-rtc-regression.sh" - ] - }, - { - "name": "tools/aurora-userpages/userpages.c", - "group": "tools", - "argv": [ - "cc", - "-O2", - "-g", - "-Wall", - "-Wextra", - "-Werror", - "-Wl,-z,max-page-size=65536", - "tools/aurora-userpages/userpages.c", - "-o", - "{output}" - ] - }, - { - "name": "tools/testing/apple/check-backlight.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-backlight.py", - "--output", - "{output}" - ], - "known_failure": "its include path lacks linux/math64.h, which dcp_backlight.h now includes" - }, - { - "name": "tools/testing/apple/check-backlight-retry.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-backlight-retry.py", - "--output", - "{output}" - ], - "known_failure": "the extracted code now calls min(), div_u64() and mul_u32_u32(), which the fixture does not mock" - }, - { - "name": "tools/testing/apple/check-lifetime.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-lifetime.py", - "--output", - "{output}" - ], - "known_failure": "the apple_dcp_afkep mock lacks lock and stopping, and spin_lock_irqsave is not mocked" - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh", - "group": "tools", - "argv": [ - "sh", - "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh" - ], - "known_failure": "the send and edid stages predate afk.c changes (afk_send_command_common, reply_len)" - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", - "group": "tools", - "argv": [ - "sh", - "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", - "--output", - "{output}" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", - "-v" - ] - }, - { - "name": "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", - "-v" - ] - }, - { - "name": "tools/asahi/tb-dp-repair-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tb-dp-repair-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-clock-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-clock-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-wiring-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-wiring-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/tiled-split-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tiled-split-tests.py", - "--mutations", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-pmp-supplier-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-pmp-supplier-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/atc-usb3-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/atc-usb3-tests.py", - "--mutations", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/g15-input-error-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-input-error-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-queue-credit-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-queue-credit-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-probe-init-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-probe-init-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-retention-tests.py", - "group": "tools", - "commits": [ - "31da169251bd34d5810a0874563d9a019b85bf56" - ], - "argv": [ - "python3", - "tools/asahi/g15-retention-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual G15 and KBox ownership bodies with drop-count allocation failures; old-source P1 controls must fail at runtime. Public synthetic inputs only." - }, - { - "name": "tools/asahi/g15-maintained-handoff-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-maintained-handoff-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-zero-words-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-zero-words-tests.py", - "--mutations", - "--evidence", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-pb-growth-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-pb-growth-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-stats-drain-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-stats-drain-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-display-order-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-display-order-tests.py" - ], - "rust": "1.93.1", - "note": "Actual display publication, T6030/T6031 link and admit() bodies on OF/device-link and admission-order models; C and admit() mutants must build and fail. No hardware or private fixtures." - }, - { - "name": "tools/testing/selftests/drivers/gpu/asahi-g15/run.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/asahi-g15/run.py" - ], - "rust": "1.93.1", - "note": "Public synthetic Rust/C host cases; two unavailable private-hardware fixture cases remain explicitly ignored." - }, - { - "name": "tools/asahi/t6030-pmp-dt-tests.py", - "group": "tools", - "setup": [ - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "allnoconfig" - ], - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "scripts_dtc" - ] - ], - "argv": [ - "python3", - "tools/asahi/t6030-pmp-dt-tests.py", - "--dtc", - "{output}/kbuild/scripts/dtc/dtc", - "--output", - "{output}/dtbs" - ], - "note": "Compile J514S/J516S default and static PMP profiles; check supplier nodes and alias in the resulting DTBs." - }, - { - "name": "tools/asahi/t6030-pmp-graph-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-pmp-graph-tests.py", - "--mutations", - "--output", - "{output}" - ], - "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." - }, - { - "name": "tools/asahi/t6030-display-admission-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-display-admission-tests.py", - "--mutations", - "--output", - "{output}" - ], - "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." - }, - { - "name": "tools/asahi/t6030-display-linkage-tests.py", - "group": "tools", - "setup": [ - [ - "make", - "-s", - "O={output}-conf", - "allnoconfig" - ] - ], - "argv": [ - "python3", - "tools/asahi/t6030-display-linkage-tests.py", - "--conf", - "{output}-conf/scripts/kconfig/conf", - "--output", - "{output}" - ], - "note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations." + "makepkg", + "python3-gi", + "zstd" + ], + "discovery": [ + "tools/asahi", + "tools/asahi-m3", + "tools/aurora-*", + "tools/testing/apple", + "tools/testing/selftests/drivers/apple", + "tools/testing/selftests/drivers/gpu/apple-*", + "tools/testing/selftests/drivers/gpu/asahi-g15" + ], + "suites": [ + { + "name": "tools/asahi/apple-dart-fw-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/apple-dart-fw-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/apple-dart-range-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/apple-dart-range-tests.py" + ], + "known_failure": "its mocks predate the current apple-dart.c (no u32, fw_mirror/fw_handoff, apple_dart_fw_invalidate; apple_dart_hw_sync_locked takes three arguments now)" + }, + { + "name": "tools/asahi/avd-codec-tail-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-codec-tail-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py --mutation round-up", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}", + "--mutation", + "round-up" + ], + "expect": "fail", + "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py --mutation cpu-unaligned", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}", + "--mutation", + "cpu-unaligned" + ], + "expect": "fail", + "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" + }, + { + "name": "tools/asahi/bcm4377-id-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-id-tests.py", + "--out", + "{output}", + "--old-ref", + "87ddae1397f6f70817cb57df76477a86bdf96424" + ], + "commits": [ + "87ddae1397f6f70817cb57df76477a86bdf96424" + ] + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation epoch", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "epoch" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation close", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "close" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation producer-fence", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "producer-fence" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation active-fence", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "active-fence" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation board", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "board" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --old-ref 36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--old-ref", + "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" + ], + "expect": "pass", + "note": "self-checking: original driver (87ddae1397f6^) must abort cases 0,1,2,28", + "commits": [ + "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" + ] + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation short-ie", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "short-ie" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation header-length", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "header-length" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation skb-capacity", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "skb-capacity" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation cmplt-header", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "cmplt-header" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --old-association f58ae50192550e6fc67c956f361f525881a1f969", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--old-association", + "f58ae50192550e6fc67c956f361f525881a1f969" + ], + "expect": "fail", + "note": "inverted; old getter from ecd1ea4d0f06^", + "commits": [ + "f58ae50192550e6fc67c956f361f525881a1f969" + ] + }, + { + "name": "tools/asahi/brcmf-bss-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-bss-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmfmac-scan-timeout-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmfmac-scan-timeout-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmfmac-scan-timeout-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmfmac-scan-timeout-tests.py", + "--output", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; cfg80211.c from f58ae5019255^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 661c0a5740e8f36816f7e79a784c36c026097368:drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "661c0a5740e8f36816f7e79a784c36c026097368" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation narrow-stream", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "narrow-stream" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation ignore-aperture", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "ignore-aperture" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation revision-wide", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "revision-wide" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --old-dma 654386a164a1e93063f879491f10bebfc1439291", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--old-dma", + "654386a164a1e93063f879491f10bebfc1439291" + ], + "expect": "fail", + "note": "inverted; probe from dbdeefb63947^", + "commits": [ + "654386a164a1e93063f879491f10bebfc1439291" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --firmware-revision b0de24a8c59735f21023c93592dd9a2eb9e155a4", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--firmware-revision", + "b0de24a8c59735f21023c93592dd9a2eb9e155a4" + ], + "expect": "fail", + "note": "inverted; firmware table from 2d17f269503c^", + "commits": [ + "b0de24a8c59735f21023c93592dd9a2eb9e155a4" + ] + }, + { + "name": "tools/asahi/brcmf-roam-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-roam-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/compute-preempt-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/compute-preempt-tests.py", + "--out", + "{output}" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/compute-preempt-tests.py --old-ref f885ea0de2cd638bda1e0d3d38a901d927a503f5", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/compute-preempt-tests.py", + "--out", + "{output}", + "--old-ref", + "f885ea0de2cd638bda1e0d3d38a901d927a503f5" + ], + "rust": "1.85.0", + "expect": "pass", + "note": "self-checking; policy from c6f4ea502bdd^", + "commits": [ + "f885ea0de2cd638bda1e0d3d38a901d927a503f5" + ] + }, + { + "name": "tools/asahi/dcp14-backlight-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-backlight-tests.py" + ] + }, + { + "name": "tools/asahi/dcp14-backlight-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-backlight-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking (-DOLD); iomfb_v14_7.c from 2e5a8e777ada^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show e19eb212f57ec32db9026666c89dd610062e9f24:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "e19eb212f57ec32db9026666c89dd610062e9f24" + ] + }, + { + "name": "tools/asahi/dcp14-panel-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-panel-tests.py" + ] + }, + { + "name": "tools/asahi/dcp14-panel-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-panel-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; iomfb_v14_7.c from 7d5a612eceaa^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show a5713d126ea098fd3b6b833fc4022762540aac6c:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "a5713d126ea098fd3b6b833fc4022762540aac6c" + ] + }, + { + "name": "tools/asahi/devcoredump-init-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/devcoredump-init-tests.py" + ] + }, + { + "name": "tools/asahi/devcoredump-init-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/devcoredump-init-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking (-DOLD); devcoredump.c from ebd3dc459ac1^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show fb7cca304cb381dc589fadbed479c6f3b36e5e23:drivers/base/devcoredump.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "fb7cca304cb381dc589fadbed479c6f3b36e5e23" + ] + }, + { + "name": "tools/asahi/display26-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/display26-tests.py" + ] + }, + { + "name": "tools/asahi/dma-heap-fd-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dma-heap-fd-tests.py", + "--out", + "{output}", + "--old-ref", + "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" + ], + "commits": [ + "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" + ] + }, + { + "name": "tools/asahi/mt7932-association-events-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-association-events-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/mt7932-association-events-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-association-events-tests.py", + "--out", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; link.c from c980f7435d50^ (the fix). The test commit itself (f885ea0de2cd) is test-only, so its parent is NOT a valid old source.", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 818bde483e1618783005ac76e1c58057a4bb4cfa:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "818bde483e1618783005ac76e1c58057a4bb4cfa" + ] + }, + { + "name": "tools/asahi/mt7932-connect-result-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-connect-result-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/mt7932-connect-result-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-connect-result-tests.py", + "--output", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; link.c from 6c3ee18a0725^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 7db4eadeeba6512726fc527cb624ae99742de607:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "7db4eadeeba6512726fc527cb624ae99742de607" + ] + }, + { + "name": "tools/asahi/mt7932-owe-retry-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-owe-retry-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py --mutation", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}", + "--mutation" + ], + "rust": "1.85.0", + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py --old", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}", + "--old" + ], + "rust": "1.85.0", + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation sep-syscore", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "sep-syscore" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation wake-irq", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "wake-irq" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation stale-policy", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "stale-policy" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --old-usb", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--old-usb" + ], + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py --old-regulatory", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--old-regulatory" + ], + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/pmp-j613-hwmon-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/pmp-j613-hwmon-tests.py" + ] + }, + { + "name": "tools/asahi/qspi-nvram-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/qspi-nvram-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate admission", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "admission" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate channel", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "channel" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate counter", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "counter" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate reset", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "reset" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --ref 0cee491386e3dff5a09b60d5703f6a7be71b0883", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--ref", + "0cee491386e3dff5a09b60d5703f6a7be71b0883" + ], + "expect": "fail", + "note": "inverted; rtkit.c from 1369d18b4ec7^", + "commits": [ + "0cee491386e3dff5a09b60d5703f6a7be71b0883" + ] + }, + { + "name": "tools/asahi/tbnet-refill-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant refill-count", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "refill-count" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant no-retry", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "no-retry" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant no-cancel", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "no-cancel" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant worker-allocation", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "worker-allocation" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --ref f7f5fc8e97cf5238c8669d9fca0d024b40174209", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--ref", + "f7f5fc8e97cf5238c8669d9fca0d024b40174209" + ], + "expect": "fail", + "note": "inverted; main.c from 71700a159448^", + "commits": [ + "f7f5fc8e97cf5238c8669d9fca0d024b40174209" + ] + }, + { + "name": "tools/asahi/tunnel-route-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tunnel-route-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/m3-batching-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-batching-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/m3-runtime-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-runtime-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/pmp-bootargs-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/pmp-bootargs-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/pmp-power-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/pmp-power-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi-m3/test_completion_progress.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test_completion_progress.py" + ] + }, + { + "name": "tools/asahi-m3/test_fault_snapshot.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test_fault_snapshot.py" + ] + }, + { + "name": "tools/asahi-m3/test-retirement-progress.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test-retirement-progress.py" + ] + }, + { + "name": "tools/asahi-m3/test-gem-object-size.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test-gem-object-size.py" + ], + "rust": "1.85.0" + }, + { + "name": "tools/aurora-sep/tests/test_adt_extract.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_adt_extract.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_air_gpu.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_air_gpu.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_desktop_fixes.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_desktop_fixes.py", + "-v" + ], + "commits": [ + "05d6db3921dec6a6667a7ec8b1b0c75f0f66bafe" + ] + }, + { + "name": "tools/aurora-sep/tests/test_diag_sensor.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_diag_sensor.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_download_resume.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_download_resume.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_esp_history.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_esp_history.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_extract_mesa_calibration.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_extract_mesa_calibration.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_frozen_dependencies.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_frozen_dependencies.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_host_entropy.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_host_entropy.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_host_state_check.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_host_state_check.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m1n1_rollback.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m1n1_rollback.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air_default.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air_default.py", + "-v" + ], + "commits": [ + "4f2a68f8b88a6b407f072356c248d22f076b401a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air_mesa.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air_mesa.py", + "-v" + ], + "commits": [ + "4f2a68f8b88a6b407f072356c248d22f076b401a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_boot_profile.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_boot_profile.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_cmdline.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_cmdline.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_flow.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_flow.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_gpu_check.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_gpu_check.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_handoff.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_handoff.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_kernel_only.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_kernel_only.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_max.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_max.py", + "-v" + ], + "commits": [ + "2ca6ff6e3480e11dfd51b720763c0fbd99fa015e", + "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc" + ], + "serial": true + }, + { + "name": "tools/aurora-sep/tests/test_m3_persistent.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_persistent.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_pro_mesa.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_pro_mesa.py", + "-v" + ], + "commits": [ + "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc", + "df0c433027e9eb283b14a56894efcc817c8a3cfb", + "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_pro_perf.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_pro_perf.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_report.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_report.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_stack_assembly.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_stack_assembly.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3max_kit.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3max_kit.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_neo_gpu.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_neo_gpu.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_packaging.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_packaging.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_pm_domains.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_pm_domains.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_ready_pam_hint.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_ready_pam_hint.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_refkey_seal_admission.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_refkey_seal_admission.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_release.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_release.py", + "-v" + ], + "commits": [ + "d14f756c9a65752ca532b93391f775387ba4f191", + "df0c433027e9eb283b14a56894efcc817c8a3cfb", + "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sbio_exchange.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sbio_exchange.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sensor_recheck.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sensor_recheck.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sensor_wake.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sensor_wake.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sep_ready.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sep_ready.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_store_shipped.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_store_shipped.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_t8112_sep_dma.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_t8112_sep_dma.py", + "-v" + ], + "setup": [ + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "allnoconfig" + ], + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "scripts_dtc" + ] + ], + "env": { + "DTC": "{output}/kbuild/scripts/dtc/dtc" + } + }, + { + "name": "tools/aurora-sep/tests/test_t8122_sep.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_t8122_sep.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_unsupported_soc.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_unsupported_soc.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_verify_bringup.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_verify_bringup.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_xart_locator.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_xart_locator.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (sh)", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (bash)", + "group": "tools", + "argv": [ + "bash", + "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "group": "tools", + "setup": [ + [ + "cc", + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + "-pedantic", + "-O2", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "-ldl", + "-o", + "{output}" + ] + ], + "argv": [ + "{output}" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh" + ] + }, + { + "name": "tools/aurora-display-notifier/tests/test_notifier.py", + "group": "tools", + "argv": [ + "make", + "-C", + "tools/aurora-display-notifier/", + "test" + ] + }, + { + "name": "tools/aurora-display-notifier/tests/integration_dbus.py", + "group": "tools", + "argv": [ + "dbus-run-session", + "--", + "env", + "AURORA_TEST_PRIVATE_BUS=1", + "python3", + "tools/aurora-display-notifier/tests/integration_dbus.py" + ] + }, + { + "name": "tools/aurora-tb/pr14-regression.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-tb/pr14-regression.py", + "--negative-controls" + ] + }, + { + "name": "tools/aurora-tb/tbnet-lifecycle-test.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-tb/tbnet-lifecycle-test.py", + "--source", + ".", + "--output", + "{output}", + "--negative-controls" + ], + "known_failure": "its fixture predates the receive refill retry (1d25feb6bb33): no rx_refill_work member or cancel_delayed_work_sync mock" + }, + { + "name": "tools/aurora-wake/test-s2idle-rtc-regression.sh", + "group": "tools", + "argv": [ + "bash", + "tools/aurora-wake/test-s2idle-rtc-regression.sh" + ] + }, + { + "name": "tools/aurora-userpages/userpages.c", + "group": "tools", + "argv": [ + "cc", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Werror", + "-Wl,-z,max-page-size=65536", + "tools/aurora-userpages/userpages.c", + "-o", + "{output}" + ] + }, + { + "name": "tools/testing/apple/check-backlight.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-backlight.py", + "--output", + "{output}" + ], + "known_failure": "its include path lacks linux/math64.h, which dcp_backlight.h now includes" + }, + { + "name": "tools/testing/apple/check-backlight-retry.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-backlight-retry.py", + "--output", + "{output}" + ], + "known_failure": "the extracted code now calls min(), div_u64() and mul_u32_u32(), which the fixture does not mock" + }, + { + "name": "tools/testing/apple/check-lifetime.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-lifetime.py", + "--output", + "{output}" + ], + "known_failure": "the apple_dcp_afkep mock lacks lock and stopping, and spin_lock_irqsave is not mocked" + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh", + "group": "tools", + "argv": [ + "sh", + "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh" + ], + "known_failure": "the send and edid stages predate afk.c changes (afk_send_command_common, reply_len)" + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", + "group": "tools", + "argv": [ + "sh", + "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", + "--output", + "{output}" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", + "-v" + ] + }, + { + "name": "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", + "-v" + ] + }, + { + "name": "tools/asahi/tb-dp-repair-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tb-dp-repair-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-clock-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-clock-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-wiring-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-wiring-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/tiled-split-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tiled-split-tests.py", + "--mutations", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-pmp-supplier-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-pmp-supplier-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/atc-usb3-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/atc-usb3-tests.py", + "--mutations", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/g15-input-error-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-input-error-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-queue-credit-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-queue-credit-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-probe-init-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-probe-init-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-retention-tests.py", + "group": "tools", + "commits": [ + "31da169251bd34d5810a0874563d9a019b85bf56" + ], + "argv": [ + "python3", + "tools/asahi/g15-retention-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual G15 and KBox ownership bodies with drop-count allocation failures; old-source P1 controls must fail at runtime. Public synthetic inputs only." + }, + { + "name": "tools/asahi/g15-maintained-handoff-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-maintained-handoff-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-zero-words-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-zero-words-tests.py", + "--mutations", + "--evidence", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-pb-growth-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-pb-growth-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-stats-drain-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-stats-drain-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-display-order-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-display-order-tests.py" + ], + "rust": "1.93.1", + "note": "Actual display publication, T6030/T6031 link and admit() bodies on OF/device-link and admission-order models; C and admit() mutants must build and fail. No hardware or private fixtures." + }, + { + "name": "tools/testing/selftests/drivers/gpu/asahi-g15/run.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/asahi-g15/run.py" + ], + "rust": "1.93.1", + "note": "Public synthetic Rust/C host cases; two unavailable private-hardware fixture cases remain explicitly ignored." + }, + { + "name": "tools/asahi/t6030-pmp-dt-tests.py", + "group": "tools", + "setup": [ + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "allnoconfig" + ], + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "scripts_dtc" + ] + ], + "argv": [ + "python3", + "tools/asahi/t6030-pmp-dt-tests.py", + "--dtc", + "{output}/kbuild/scripts/dtc/dtc", + "--output", + "{output}/dtbs" + ], + "note": "Compile J514S/J516S default and static PMP profiles; check supplier nodes and alias in the resulting DTBs." + }, + { + "name": "tools/asahi/t6030-pmp-graph-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-pmp-graph-tests.py", + "--mutations", + "--output", + "{output}" + ], + "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." + }, + { + "name": "tools/asahi/t6030-display-admission-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-display-admission-tests.py", + "--mutations", + "--output", + "{output}" + ], + "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." + }, + { + "name": "tools/asahi/t6030-display-linkage-tests.py", + "group": "tools", + "setup": [ + [ + "make", + "-s", + "O={output}-conf", + "allnoconfig" + ] + ], + "argv": [ + "python3", + "tools/asahi/t6030-display-linkage-tests.py", + "--conf", + "{output}-conf/scripts/kconfig/conf", + "--output", + "{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" + ] + }, + { + "name": "tools/asahi/test-m3-mailbox.py", + "group": "tools", + "argv": [ + "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" + }, + { + "name": "tools/asahi/test-m3-power.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-power.py" + ], + "rust": "1.85.0" + } + ], + "helpers": { + "tools/testing/selftests/drivers/gpu/apple-dcp/run.py": "the implementation behind tools/testing/selftests/drivers/gpu/apple-dcp/run.sh, which is listed", + "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/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.", + "tools/aurora-sep/tests/coordinator/deletion-check.sh": "Needs /dev/sep-bio, fprintd over the system bus and a previously enrolled expendable test account; spans a reboot.", + "tools/aurora-sep/tests/coordinator/sep-live-count.py": "Opens /dev/sep-bio on a running kernel. Not picked up by `unittest discover` (no test_ prefix) - keep it that way.", + "tools/aurora-wake/s2idle-rtc-regression.sh": "Real suspend-to-idle cycles with rtcwake on an Apple machine, as root. Its argument checks run hermetically via test-s2idle-rtc-regression.sh (in suites.json).", + "tools/aurora-wake/attended-bt-wake.sh": "J700 with a paired Bluetooth keyboard, a person pressing keys, root.", + "tools/aurora-wake/bt-wake-cycle.sh": "One s2idle cycle on a J700 with Bluetooth wake, root.", + "tools/aurora-wake/bt-wake-power.sh": "Alternating RTC-timed sleeps on a J700, root.", + "tools/aurora-wake/bt-wake-emulated.sh": "Drives two other machines over SSH (target and a BlueZ host).", + "tools/aurora-wake/hog-keyboard.py": "BLE HID keyboard emulator for a second BlueZ host (system bus + Bluetooth adapter).", + "tools/aurora-tb/tb-pcie-report": "Reads live Thunderbolt/PCIe state and the kernel log with a dock attached (may need sudo).", + "tools/aurora-m3/m3-serial.py": "macOS helper (sudo, macvdmtool) recording a second Mac's serial console over a USB-C cable.", + "tools/testing/selftests/drivers/apple/j813-peripherals.sh": "Physical J813 check (exits 4 unless /proc/device-tree/compatible has apple,j813 and euid 0).", + "tools/testing/selftests/drivers/net/mt7932_e2e.py": "End-to-end Wi-Fi on a J700 via nmcli and the real network. Its helper unit tests (mt7932_e2e_test.py) are in suites.json.", + "tools/testing/selftests/arm64/user4k/": "kselftest programs for the user4k granule API: need an arm64 Aurora kernel booted with user4k=on (and an aarch64 toolchain to build). check-any-quarter.py / runtime-app.py consume output of those programs.", + "tools/aurora-sep/recipes/mesa-m3/tests/test-probe.sh": "Needs a built mesa-m3-probe (C, links Mesa/EGL/Wayland) and a headless weston for the wayland case.", + "tools/aurora-sep/recipes/mesa-m3/tests/test-confirm.sh": "Needs a built mesa-m3-wlcheck (Wayland client) passed as its argument.", + "tools/aurora-sep/recipes/mesa-m3/tests/build-checks.sh": "Runs inside the PKGBUILD package() step against a freshly built Mesa tree (makepkg, aarch64).", + "tools/aurora-bindgen/build.py": "Downloads the pinned bindgen 0.73.2 source archive (codeload.github.com) and crates.io dependencies, then cargo build --locked -j2 (89 s here incl. its own test.py run; passed). Viable as an optional cached job; GitHub runners have network.", + "tools/aurora-bindgen/test.py": "Hermetic once a patched bindgen exists (--bindgen PATH, 2 s, passed). --baseline-bindgen must be an unpatched 0.73.2; the system 0.73.1 fails the tag-only equality check by design.", + "tools/asahi-m3/completion_progress.py, fault_snapshot.py, retirement_progress.py": "Libraries that read /sys/kernel/debug/asahi-m3/* or /dev/kmsg on an M3 machine. Their offline tests are in suites.json.", + "tools/aurora-sep/air-gpu/*.sh, tools/aurora-sep/pro/*, tools/aurora-sep/neo/*": "On-device helpers; exercised through fixtures by tools/aurora-sep/tests (test_air_gpu, test_m3_pro_perf, test_neo_gpu).", + "drivers/**/*-test.c, *_test.c, tools/testing/kunit/configs/{apple_firmware,arm64_user4k*}.config": "KUnit suites: need a kernel build and tools/testing/kunit/kunit.py (arm64 under QEMU). Not host scripts.", + "tools/aurora-sep/tests: env-gated cases": "Skipped unless set: AURORA_ADT_FIXTURE (real ADT dump), AIR_GPU_TEST_VULKAN=1 (real Vulkan job on a render node), AURORA_PRO_MESA_PKG / AURORA_M1N1_PKG (release packages); test_m3max_kit real-package cases need /nonexistent/... packages. The rest of each module runs." } - ], - "helpers": { - "tools/testing/selftests/drivers/gpu/apple-dcp/run.py": "the implementation behind tools/testing/selftests/drivers/gpu/apple-dcp/run.sh, which is listed", - "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" - }, - "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.", - "tools/aurora-sep/tests/coordinator/deletion-check.sh": "Needs /dev/sep-bio, fprintd over the system bus and a previously enrolled expendable test account; spans a reboot.", - "tools/aurora-sep/tests/coordinator/sep-live-count.py": "Opens /dev/sep-bio on a running kernel. Not picked up by `unittest discover` (no test_ prefix) - keep it that way.", - "tools/aurora-wake/s2idle-rtc-regression.sh": "Real suspend-to-idle cycles with rtcwake on an Apple machine, as root. Its argument checks run hermetically via test-s2idle-rtc-regression.sh (in suites.json).", - "tools/aurora-wake/attended-bt-wake.sh": "J700 with a paired Bluetooth keyboard, a person pressing keys, root.", - "tools/aurora-wake/bt-wake-cycle.sh": "One s2idle cycle on a J700 with Bluetooth wake, root.", - "tools/aurora-wake/bt-wake-power.sh": "Alternating RTC-timed sleeps on a J700, root.", - "tools/aurora-wake/bt-wake-emulated.sh": "Drives two other machines over SSH (target and a BlueZ host).", - "tools/aurora-wake/hog-keyboard.py": "BLE HID keyboard emulator for a second BlueZ host (system bus + Bluetooth adapter).", - "tools/aurora-tb/tb-pcie-report": "Reads live Thunderbolt/PCIe state and the kernel log with a dock attached (may need sudo).", - "tools/aurora-m3/m3-serial.py": "macOS helper (sudo, macvdmtool) recording a second Mac's serial console over a USB-C cable.", - "tools/testing/selftests/drivers/apple/j813-peripherals.sh": "Physical J813 check (exits 4 unless /proc/device-tree/compatible has apple,j813 and euid 0).", - "tools/testing/selftests/drivers/net/mt7932_e2e.py": "End-to-end Wi-Fi on a J700 via nmcli and the real network. Its helper unit tests (mt7932_e2e_test.py) are in suites.json.", - "tools/testing/selftests/arm64/user4k/": "kselftest programs for the user4k granule API: need an arm64 Aurora kernel booted with user4k=on (and an aarch64 toolchain to build). check-any-quarter.py / runtime-app.py consume output of those programs.", - "tools/aurora-sep/recipes/mesa-m3/tests/test-probe.sh": "Needs a built mesa-m3-probe (C, links Mesa/EGL/Wayland) and a headless weston for the wayland case.", - "tools/aurora-sep/recipes/mesa-m3/tests/test-confirm.sh": "Needs a built mesa-m3-wlcheck (Wayland client) passed as its argument.", - "tools/aurora-sep/recipes/mesa-m3/tests/build-checks.sh": "Runs inside the PKGBUILD package() step against a freshly built Mesa tree (makepkg, aarch64).", - "tools/aurora-bindgen/build.py": "Downloads the pinned bindgen 0.73.2 source archive (codeload.github.com) and crates.io dependencies, then cargo build --locked -j2 (89 s here incl. its own test.py run; passed). Viable as an optional cached job; GitHub runners have network.", - "tools/aurora-bindgen/test.py": "Hermetic once a patched bindgen exists (--bindgen PATH, 2 s, passed). --baseline-bindgen must be an unpatched 0.73.2; the system 0.73.1 fails the tag-only equality check by design.", - "tools/asahi-m3/completion_progress.py, fault_snapshot.py, retirement_progress.py": "Libraries that read /sys/kernel/debug/asahi-m3/* or /dev/kmsg on an M3 machine. Their offline tests are in suites.json.", - "tools/aurora-sep/air-gpu/*.sh, tools/aurora-sep/pro/*, tools/aurora-sep/neo/*": "On-device helpers; exercised through fixtures by tools/aurora-sep/tests (test_air_gpu, test_m3_pro_perf, test_neo_gpu).", - "drivers/**/*-test.c, *_test.c, tools/testing/kunit/configs/{apple_firmware,arm64_user4k*}.config": "KUnit suites: need a kernel build and tools/testing/kunit/kunit.py (arm64 under QEMU). Not host scripts.", - "tools/aurora-sep/tests: env-gated cases": "Skipped unless set: AURORA_ADT_FIXTURE (real ADT dump), AIR_GPU_TEST_VULKAN=1 (real Vulkan job on a render node), AURORA_PRO_MESA_PKG / AURORA_M1N1_PKG (release packages); test_m3max_kit real-package cases need /nonexistent/... packages. The rest of each module runs." - } }