Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
/* G16 is admitted on non-coherent DMA only. */
dma-noncoherent;
apple,pmp = <&pmp>;
apple,clpc = <&clpc>;
memory-region = <&gpu_ttbs>, <&gpu_pagetables>, <&gpu_handoff>,
<&gpu_shared_l2>, <&gpu_fw_text>, <&gpu_fw_data>;
memory-region-names = "ttbs", "pagetables", "handoff",
Expand Down
1 change: 1 addition & 0 deletions drivers/gpu/drm/asahi/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ config DRM_ASAHI_G16
bool "Apple M4 (G16G) support (experimental)"
depends on DRM_ASAHI && ARM64
depends on APPLE_PMP=y || APPLE_PMP=DRM_ASAHI
depends on APPLE_CLPC=y || APPLE_CLPC=DRM_ASAHI || APPLE_CLPC=n
depends on THERMAL_OF
help
Rust backend for the qualified J713/T8132 GPU firmware.
Expand Down
5 changes: 5 additions & 0 deletions drivers/gpu/drm/asahi/g16/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,9 @@ impl Firmware {
if !self.engine.chan_wait {
self.recovery.publication_wait_deadline = None;
}
// Polling retires before it publishes, so this sees work start and
// drain; a fail-stop reports from fail_flights().
self.power.report_work(!self.engine.flights.is_empty());
result
}

Expand Down Expand Up @@ -1358,5 +1361,7 @@ impl Firmware {
s.starved_ns += s.pub_ns.wrapping_sub(s.starve_from_ns);
}
s.starve_from_ns = 0;
let now = s.pub_ns;
self.power.utilization.busy(now);
}
}
33 changes: 33 additions & 0 deletions drivers/gpu/drm/asahi/g16/glue.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <linux/of_reserved_mem.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/soc/apple/clpc.h>
#include <linux/soc/apple/pmp-display.h>
#include <linux/soc/apple/pmp-cooling.h>
#include <linux/soc/apple/pmp-telemetry.h>
Expand Down Expand Up @@ -46,6 +47,9 @@ void asahi_g16_cooling_unregister(struct asahi_g16_cooling *cooling);
int asahi_g16_cooling_sample(struct asahi_g16_cooling *cooling, u64 now,
long *temperature, u32 *governor, u32 *power,
u32 *backstop);
int asahi_g16_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out);
void asahi_g16_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active);
void asahi_g16_clpc_gpu_put(struct apple_clpc_gpu *gpu);

static int asahi_g16_cooling_max(struct thermal_cooling_device *cdev,
unsigned long *state)
Expand Down Expand Up @@ -261,3 +265,32 @@ int asahi_g16_power_domain_state(struct device *dev, u32 *offset, u32 *state)
return PTR_ERR(pmgr);
return regmap_read(pmgr, *offset, state);
}

/*
* The CLPC holding the memory floor for GPU work is a performance aid: the GPU
* works without one, only slower. A kernel without the CLPC runs without it;
* one that has not probed yet defers the GPU, with the reason recorded.
*/
int asahi_g16_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out)
{
int ret = apple_clpc_gpu_get(dev, out);

if (ret == -ENODEV) {
dev_warn(dev, "no CLPC: GPU work gets no memory floor\n");
*out = NULL;
return 0;
}
if (ret)
return dev_err_probe(dev, ret, "no CLPC for GPU work\n");
return 0;
}

void asahi_g16_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active)
{
apple_clpc_gpu_set_active(gpu, active);
}

void asahi_g16_clpc_gpu_put(struct apple_clpc_gpu *gpu)
{
apple_clpc_gpu_put(gpu);
}
21 changes: 18 additions & 3 deletions drivers/gpu/drm/asahi/g16/poll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ impl Shared {
let evented = self.evented.swap(false, Ordering::AcqRel);
match firmware.poll(evented, &mut done) {
Ok(true) => self.rearm(firmware.engine.chan_wait),
// Idle: one more pass after a window gives the state back. A failed
// engine's poll no longer services power, so it has nothing to give back.
Ok(false)
if firmware.recovery.failed().is_none()
&& firmware.power.utilization.decaying() =>
{
self.mod_poll(kernel::time::msecs_to_jiffies(
(super::power::policy::WINDOW_NS / 1_000_000) as u32,
))
}
Ok(false) => {}
Err(error) => {
firmware.fail_stop(error, &mut done);
Expand Down Expand Up @@ -535,6 +545,7 @@ impl Firmware {
if self.engine.flights.is_empty() {
// Drained: a gap until the next publication.
self.engine.stats.idle_from_ns = now_ns();
self.power.utilization.idle(self.engine.stats.idle_from_ns);
}
// The next prefix starts executing about now.
self.renew_deadlines();
Expand Down Expand Up @@ -861,9 +872,13 @@ impl Firmware {
super::recovery::logic::FIRMWARE_STAMP_PERIOD, self.engine.ordinal, self.engine.draw)?;
writeln!(
f,
"power: governor {} budget {} effective S{} temperature_mc {} updates {} sensor_errors {}",
self.power.governor, self.power.budget, self.power.effective,
self.power.temperature_mc, self.power.updates, self.power.sensor_errors
"power: governor {} budget {} demand S{} utilization {} effective S{} temperature_mc {} updates {} sensor_errors {}",
self.power.governor,
self.power.budget,
self.power.utilization.demand,
self.power.utilization.average,
self.power.effective, self.power.temperature_mc, self.power.updates,
self.power.sensor_errors
)?;
writeln!(
f,
Expand Down
77 changes: 73 additions & 4 deletions drivers/gpu/drm/asahi/g16/power.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-only OR MIT

//! PMP-owned cooling requests and retained-session performance ceiling.
//! PMP-owned cooling requests, retained-session performance ceiling and the
//! utilization governor.
//!
//! The firmware owns voltage and clock transitions through PMP. The host
//! consumes PMP-owner telemetry and publishes only three Globals selectors,
//! in hundredths of a performance state. Firmware power tables and policy
//! words from other GPU generations are never modified here.
//!
//! The firmware runs at the published state, busy or not, so the host requests
//! one from the GPU's busy share of each window, bounded by the ceiling.
//!
//! The firmware keeps GFX powered while the GPU idles, so the host reports
//! when it has work in flight to the DT-selected CLPC, which holds the memory
//! and fabric floor for that work.

pub(crate) mod policy;

Expand All @@ -23,6 +31,9 @@ static_assert!(globals::PSTATE_SELECTORS[1] == 0x8c);
static_assert!(globals::PSTATE_SELECTORS[2] == 0x98);

extern "C" {
fn asahi_g16_clpc_gpu_get(dev: *mut c_void, out: *mut *mut c_void) -> core::ffi::c_int;
fn asahi_g16_clpc_gpu_set_active(gpu: *mut c_void, active: bool);
fn asahi_g16_clpc_gpu_put(gpu: *mut c_void);
fn asahi_g16_cooling_register(dev: *mut c_void) -> *mut c_void;
fn asahi_g16_cooling_unregister(cooling: *mut c_void);
fn asahi_g16_cooling_sample(
Expand Down Expand Up @@ -96,9 +107,49 @@ struct Sample {
backstop: u32,
}

/// DT-selected memory-demand provider (`apple,clpc`), or none. Its
/// independent allocation outlives a detached provider; a device link orders
/// supplier removal and suspend.
struct Demand(*mut c_void);

// SAFETY: the provider's leaf spinlock serializes all access; no GPU callback.
unsafe impl Send for Demand {}
// SAFETY: shared access only changes that locked request, never its lifetime.
unsafe impl Sync for Demand {}

impl Demand {
fn new(dev: &device::Device) -> Result<Self> {
let mut out = core::ptr::null_mut();
// SAFETY: live platform device and writable result; C returns an owned
// reference, or NULL without an `apple,clpc` provider.
let ret = unsafe { asahi_g16_clpc_gpu_get(dev.as_raw().cast(), &mut out) };
if ret < 0 {
Err(Error::from_errno(ret))
} else {
Ok(Self(out))
}
}

fn set_active(&self, active: bool) {
// SAFETY: the retained reference is live; NULL denotes no provider.
unsafe { asahi_g16_clpc_gpu_set_active(self.0, active) };
}
}

impl Drop for Demand {
fn drop(&mut self) {
// SAFETY: releases exactly the reference returned by get, clearing
// the demand first.
unsafe { asahi_g16_clpc_gpu_put(self.0) };
}
}

/// All fields are protected by the device mutex. Before the first valid
/// provider sample the shared cooling policy keeps the ceiling at S1.
pub(crate) struct Power {
clpc: Demand,
/// The work-in-flight state last reported to `clpc`.
reported: Option<bool>,
cooling: Cooling,
sample: Sample,
last_sample_ns: Option<u64>,
Expand All @@ -113,13 +164,17 @@ pub(crate) struct Power {
pub(crate) sensor_errors: u64,
pub(crate) decisions: u64,
last_decision: Option<(u32, u32, u32, u32, Option<Error>)>,
pub(crate) utilization: policy::Governor,
}

impl Power {
/// The Main image initializes all nine active states. Initdata publishes
/// startup S1 before INIT; this object starts with that same effective state.
pub(crate) fn new(dev: &device::Device) -> Result<Self> {
Ok(Self {
// First: a CLPC that has not probed yet defers before the PMP vote.
clpc: Demand::new(dev)?,
reported: None,
cooling: Cooling::new(dev)?,
sample: Sample {
temperature_mc: 0,
Expand All @@ -139,9 +194,19 @@ impl Power {
sensor_errors: 0,
decisions: 0,
last_decision: None,
utilization: policy::Governor::new(),
})
}

/// Reports a change of work in flight to the CLPC: the memory and fabric
/// floor follows it, not the GFX power state.
pub(crate) fn report_work(&mut self, in_flight: bool) {
if self.reported != Some(in_flight) {
self.clpc.set_active(in_flight);
self.reported = Some(in_flight);
}
}

fn ceiling(&mut self, now: u64) -> (u32, bool) {
if policy::read_due(self.last_sample_ns, now) {
self.last_sample_ns = Some(now);
Expand Down Expand Up @@ -184,6 +249,10 @@ impl Firmware {
}
let now = now_ns();
let (limit, decision) = self.power.ceiling(now);
let target = self
.power
.utilization
.govern(now, limit, !self.engine.flights.is_empty());
if decision {
dev_info!(
self.dev(),
Expand All @@ -197,15 +266,15 @@ impl Firmware {
limit
);
}
if limit == self.power.effective {
if target == self.power.effective {
return Ok(());
}
let globals = self.init.globals.as_ref().ok_or(ENODEV)?;
for offset in globals::PSTATE_SELECTORS {
globals.w32(offset, limit * 100)?;
globals.w32(offset, target * 100)?;
}
wmb();
self.power.effective = limit;
self.power.effective = target;
self.power.updates += 1;
Ok(())
}
Expand Down
Loading