blob: 9797432295b3a3ff19597926521624c39f948b95 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::Context as _;
use fidl_fuchsia_gpu_magma as fmagma;
use fidl_fuchsia_power_broker as fpbroker;
use fuchsia_async as fasync;
use fuchsia_component::client::{self as fclient, connect_to_protocol_sync};
use futures::TryStreamExt;
use itertools::Itertools;
use starnix_core::fs::sysfs::build_device_directory;
use starnix_core::task::{CurrentTask, Kernel};
use starnix_core::vfs::FsNodeOps;
use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps, parse_unsigned_file};
use starnix_logging::{log_info, log_warn};
use starnix_sync::{LockDepMutex, MagmaPowerDomainLock};
use starnix_uapi::errors::Errno;
use starnix_uapi::file_mode::mode;
use starnix_uapi::{errno, error};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
/// Sysfs name of the PowerVR GPU device (`0x34f00000`).
const DEFAULT_GPU_DEVICE_NAME: &str = "34f00000.gpu0";
/// Sysfs name of the GPU runtime PM genpd device (`GPUPowerOn`).
const DEFAULT_GPU_GENPD_DEVICE_NAME: &str = "genpd:0:34f00000.gpu0";
/// Maximum time to wait for synchronous FIDL calls to the Magma driver or Power Broker.
const FIDL_CALL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
fn fidl_deadline() -> zx::MonotonicInstant {
zx::MonotonicInstant::after(FIDL_CALL_TIMEOUT)
}
// TODO(https://fxbug.dev/564595867): Query the operating point frequency table from
// `fuchsia.gpu.magma.PowerElementProvider` once a frequency-table FIDL method is added.
/// 15-state GPU operating point frequency table sorted ascending in Hz (`PF14` .. `PF0`).
const DEFAULT_GPU_AVAILABLE_FREQUENCIES_HZ: &[u64] = &[
198_000_000, // PF14
253_000_000, // PF13
300_000_000, // PF12
396_000_000, // PF11 (boot default)
448_000_000, // PF10
512_000_000, // PF9
576_000_000, // PF8 (LAUNCH / EXPENSIVE_RENDERING)
633_000_000, // PF7
691_000_000, // PF6
748_000_000, // PF5
806_000_000, // PF4
864_000_000, // PF3
921_000_000, // PF2
998_000_000, // PF1
1_094_000_000, // PF0
];
const DEFAULT_GPU_FREQ_HZ: u64 = 396_000_000;
const DEFAULT_POLLING_INTERVAL_MS: u64 = 20;
/// Power Broker level when the GPU is powered off (`OFF`).
#[cfg_attr(not(test), allow(dead_code))]
const GPU_POWER_LEVEL_OFF: fpbroker::PowerLevel = 0;
/// Power Broker level when the GPU is powered on and ready for work (`ON_READY_FOR_WORK`).
const GPU_POWER_LEVEL_ON: fpbroker::PowerLevel = 1;
/// Container-provided configuration for GPU sysfs device registration and operating point
/// frequencies.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GpuDeviceConfig {
/// Linux platform/devfreq sysfs device name (e.g. `"34f00000.gpu0"`).
pub device_name: String,
/// Linux runtime PM genpd sysfs device name (e.g. `"genpd:0:34f00000.gpu0"`).
pub genpd_device_name: String,
// TODO(https://fxbug.dev/564595867): Query `available_frequencies_hz` from
// `fuchsia.gpu.magma.PowerElementProvider` once a frequency-table FIDL method is added.
/// Operating point frequencies sorted ascending in Hz.
pub available_frequencies_hz: Vec<u64>,
/// Default / baseline GPU clock frequency in Hz when no floor vote exceeds it.
pub default_freq_hz: u64,
}
impl Default for GpuDeviceConfig {
fn default() -> Self {
Self {
device_name: DEFAULT_GPU_DEVICE_NAME.to_string(),
genpd_device_name: DEFAULT_GPU_GENPD_DEVICE_NAME.to_string(),
available_frequencies_hz: DEFAULT_GPU_AVAILABLE_FREQUENCIES_HZ.to_vec(),
default_freq_hz: DEFAULT_GPU_FREQ_HZ,
}
}
}
impl GpuDeviceConfig {
fn min_freq_hz(&self) -> u64 {
self.available_frequencies_hz.first().copied().unwrap_or(self.default_freq_hz)
}
fn max_freq_hz(&self) -> u64 {
self.available_frequencies_hz.last().copied().unwrap_or(self.default_freq_hz)
}
/// Snaps a floor vote in Hz up to the smallest GPU operating point `>= hz` (clamped to the
/// maximum operating point).
fn snap_min_freq_hz(&self, hz: u64) -> Option<u64> {
if hz == 0 {
return None;
}
Some(
self.available_frequencies_hz
.iter()
.copied()
.find(|&opp_hz| opp_hz >= hz)
.unwrap_or_else(|| self.max_freq_hz()),
)
}
/// Snaps a ceiling vote in Hz down to the highest GPU operating point `<= hz` (or `None` when
/// `hz == 0` or unconstrained at or above the maximum operating point).
fn snap_max_freq_hz(&self, hz: u64) -> Option<u64> {
let max_hz = self.max_freq_hz();
if hz == 0 || hz >= max_hz {
return None;
}
Some(
self.available_frequencies_hz
.iter()
.rev()
.copied()
.find(|&opp_hz| opp_hz <= hz)
.unwrap_or_else(|| self.min_freq_hz()),
)
}
}
/// Threshold separating kHz values (e.g. `396000` / `576000` in kHz-based `powerhint.json`) from
/// Hz values (e.g. `396000000` / `576000000` in Linux `devfreq` and Hz-based `powerhint.json`).
const KHZ_TO_HZ_THRESHOLD: u64 = 10_000_000;
const VOTE_MANAGER_MIN_FREQ_NODES: &[&str] =
&["powerhint_min_freq", "soft_min_freq", "debug_min_freq"];
const VOTE_MANAGER_MAX_FREQ_NODES: &[&str] =
&["powerhint_max_freq", "thermal_max_freq", "soft_max_freq", "debug_max_freq"];
fn spawn_power_service_watcher(kernel: &Kernel, domain: Arc<GpuPowerDomain>) {
kernel.kthreads.spawn_future(
move || async move {
let service = match fclient::Service::open(fmagma::PowerServiceMarker) {
Ok(service) => service,
Err(err) => {
log_info!("GPU PowerService directory unavailable: {err:#}");
return;
}
};
if let Err(err) = watch_power_service(domain, service).await {
log_info!("GPU PowerService watcher exited: {err:#}");
}
},
"gpu_power_service_watcher",
);
}
/// Watches `service` (`fuchsia.gpu.magma.PowerService`) for provider instances and installs each
/// new `PowerElementProvider` proxy into `domain`, reconciling any active power or frequency votes.
async fn watch_power_service(
domain: Arc<GpuPowerDomain>,
service: fclient::Service<fmagma::PowerServiceMarker>,
) -> Result<(), anyhow::Error> {
let mut instances = service.watch().await.context("failed to watch GPU PowerService")?;
while let Some(instance) = instances.try_next().await? {
let name = instance.instance_name().to_string();
let provider = match instance.connect_to_power_element_provider_sync() {
Ok(provider) => Arc::new(provider),
Err(err) => {
log_warn!("Failed to connect to GPU PowerElementProvider {name}: {err:#}");
continue;
}
};
let domain_clone = Arc::clone(&domain);
fasync::unblock(move || {
domain_clone.set_provider(provider);
})
.await;
}
Ok(())
}
/// Registers GPU devfreq (`/sys/class/devfreq/<device_name>` and
/// `/sys/devices/platform/<device_name>/devfreq/<device_name>`) and genpd runtime PM
/// (`/sys/devices/<genpd_device_name>/power/control`) sysfs nodes.
pub(crate) fn register_gpu_devices(kernel: &Kernel, config: GpuDeviceConfig) {
let device_name = config.device_name.clone();
let genpd_device_name = config.genpd_device_name.clone();
let domain = Arc::new(GpuPowerDomain::new(config));
spawn_power_service_watcher(kernel, Arc::clone(&domain));
let registry = &kernel.device_registry;
let objects = &registry.objects;
// 1. `/sys/devices/<genpd_device_name>/power/control` (`GPUPowerOn`)
let domain_for_genpd = domain.clone();
registry.add_subsystemless_device(
genpd_device_name.as_str().into(),
/* parent = */ None,
move |device, dir| {
build_device_directory(device, dir);
build_gpu_power_directory(dir, &domain_for_genpd);
},
);
// 2. `/sys/devices/platform/<device_name>` + `/sys/class/devfreq/<device_name>` (`GPUMinFreq`)
let domain_for_platform = domain.clone();
let gpu_platform_device = registry.add_bus_device(
device_name.as_str().into(),
Some(objects.platform_device()),
objects.platform_bus(),
move |device, dir| {
build_device_directory(device, dir);
build_gpu_power_directory(dir, &domain_for_platform);
},
);
let devfreq_class = objects.get_or_create_class("devfreq".into());
registry.add_numberless_device(
device_name.as_str().into(),
Some(gpu_platform_device),
devfreq_class,
move |device, dir| {
build_device_directory(device, dir);
build_gpu_devfreq_directory(dir, &domain);
},
);
}
fn build_gpu_power_directory(dir: &SimpleDirectoryMutator, domain: &Arc<GpuPowerDomain>) {
dir.subdir("power", 0o755, |dir| {
dir.entry("control", GpuPowerControlFile::new_node(domain.clone()), mode!(IFREG, 0o644));
dir.entry(
"runtime_status",
GpuRuntimeStatusFile::new_node(domain.clone()),
mode!(IFREG, 0o444),
);
});
}
fn build_gpu_devfreq_directory(dir: &SimpleDirectoryMutator, domain: &Arc<GpuPowerDomain>) {
dir.entry(
"available_frequencies",
BytesFile::new_node(
(domain.config.available_frequencies_hz.iter().join(" ") + "\n").into_bytes(),
),
mode!(IFREG, 0o444),
);
dir.entry("cur_freq", GpuCurFreqFile::new_node(domain.clone()), mode!(IFREG, 0o444));
dir.entry(
"min_freq",
GpuFreqVoteFile::<false>::new_node(domain.clone(), "min_freq"),
mode!(IFREG, 0o644),
);
dir.entry(
"max_freq",
GpuFreqVoteFile::<true>::new_node(domain.clone(), "max_freq"),
mode!(IFREG, 0o644),
);
dir.entry(
"polling_interval",
GpuPollingIntervalFile::new_node(domain.clone()),
mode!(IFREG, 0o644),
);
dir.entry("governor", BytesFile::new_node(b"simple_ondemand\n".to_vec()), mode!(IFREG, 0o444));
dir.subdir("vote_manager", 0o755, |dir| {
for &voter in VOTE_MANAGER_MIN_FREQ_NODES {
dir.entry(
voter,
GpuFreqVoteFile::<false>::new_node(domain.clone(), voter),
mode!(IFREG, 0o644),
);
}
for &voter in VOTE_MANAGER_MAX_FREQ_NODES {
dir.entry(
voter,
GpuFreqVoteFile::<true>::new_node(domain.clone(), voter),
mode!(IFREG, 0o644),
);
}
});
}
/// Normalizes a GPU frequency input written in either Hz (`>= 10_000_000`, e.g. `396000000`) or
/// kHz (`< 10_000_000`, e.g. `396000` / `576000`) to Hz.
fn normalize_gpu_freq_to_hz(raw_value: u64) -> u64 {
if raw_value == 0 {
0
} else if raw_value < KHZ_TO_HZ_THRESHOLD {
raw_value.saturating_mul(1_000)
} else {
raw_value
}
}
#[derive(Debug)]
struct GpuPowerDomainState {
/// Whether `GPUPowerOn` (`/sys/devices/genpd:0:34f00000.gpu0/power/control`) is set to `"on"`.
power_control_on: bool,
/// Monotonic generation counter for `power_control_on` updates.
power_generation: u64,
/// Active floor votes (`voter -> snapped_hz`). Effective floor is `max(min_votes.values())`.
min_votes: HashMap<&'static str, u64>,
/// Monotonic generation counter for `min_votes` updates.
min_freq_generation: u64,
/// Active ceiling votes (`voter -> snapped_hz`). Effective ceiling is `min(max_votes.values())`.
max_votes: HashMap<&'static str, u64>,
/// Monotonic generation counter for `max_votes` updates.
max_freq_generation: u64,
/// Configured devfreq polling interval in ms.
polling_interval_ms: u64,
/// FIDL proxy to the Magma driver populated by `watch_power_service` (or injected in tests).
provider: Option<Arc<fmagma::PowerElementProviderSynchronousProxy>>,
/// Cached `PowerGoalType::OnReadyForWork` `DependencyToken` fetched when `provider` connects.
power_on_token: Option<zx::Event>,
/// Active Power Broker lease token keeping `gpu_power_on` at `GPU_POWER_LEVEL_ON`
/// (`ON_READY_FOR_WORK`).
active_power_lease: Option<zx::EventPair>,
/// Active Power Broker lease token holding `gpu_min_freq` at the highest active floor level.
min_freq_lease: Option<zx::EventPair>,
/// Last effective min-frequency floor (in Hz) synced to Power Broker.
applied_min_freq_hz: Option<u64>,
/// Active `PowerElementProvider::SetClockLimit` handle capping the GPU clock at the lowest
/// active ceiling vote.
max_freq_limit_handle: Option<zx::EventPair>,
/// Last effective max-frequency ceiling (in Hz) synced to `PowerElementProvider::SetClockLimit`.
applied_max_freq_hz: Option<u64>,
/// Whether the active `PowerElementProvider` supports `GetClockSpeedLevel`. Latched to `false`
/// once the driver returns `ZX_ERR_NOT_SUPPORTED` and reset when a new provider connects.
clock_speed_level_supported: bool,
/// Whether the active `PowerElementProvider` supports `SetClockLimit`. Latched to `false`
/// once the driver returns `ZX_ERR_NOT_SUPPORTED` and reset when a new provider connects.
clock_limit_supported: bool,
}
impl Default for GpuPowerDomainState {
fn default() -> Self {
Self {
power_control_on: false,
power_generation: 0,
min_votes: HashMap::new(),
min_freq_generation: 0,
max_votes: HashMap::new(),
max_freq_generation: 0,
polling_interval_ms: DEFAULT_POLLING_INTERVAL_MS,
provider: None,
power_on_token: None,
active_power_lease: None,
min_freq_lease: None,
applied_min_freq_hz: None,
max_freq_limit_handle: None,
applied_max_freq_hz: None,
clock_speed_level_supported: true,
clock_limit_supported: true,
}
}
}
impl GpuPowerDomainState {
/// Clears the active `PowerElementProvider` and all provider-bound state, returning any active
/// power or clock handles so the caller can drop them after releasing the state lock.
fn clear_provider(
&mut self,
) -> (Option<zx::EventPair>, Option<zx::EventPair>, Option<zx::EventPair>) {
self.provider = None;
self.power_on_token = None;
self.clock_speed_level_supported = true;
self.clock_limit_supported = true;
self.applied_min_freq_hz = None;
self.applied_max_freq_hz = None;
self.power_generation = self.power_generation.wrapping_add(1);
self.min_freq_generation = self.min_freq_generation.wrapping_add(1);
self.max_freq_generation = self.max_freq_generation.wrapping_add(1);
(
self.active_power_lease.take(),
self.min_freq_lease.take(),
self.max_freq_limit_handle.take(),
)
}
}
/// Coordinates GPU power state and devfreq frequency votes across Starnix sysfs nodes.
///
/// Aggregates floor (`min_freq`) and ceiling (`max_freq`) votes from `devfreq` and `vote_manager`
/// nodes alongside runtime PM (`power/control`) state, and synchronizes the effective power and
/// clock levels with the Magma driver (`fuchsia.gpu.magma.PowerElementProvider`) and Fuchsia Power
/// Broker (`fuchsia.power.broker.Topology`).
#[derive(Debug, Default)]
struct GpuPowerDomain {
config: GpuDeviceConfig,
topology: Option<fpbroker::TopologySynchronousProxy>,
state: LockDepMutex<GpuPowerDomainState, MagmaPowerDomainLock>,
}
impl GpuPowerDomain {
fn new(config: GpuDeviceConfig) -> Self {
Self {
config,
topology: connect_to_protocol_sync::<fpbroker::TopologyMarker>().ok(),
state: LockDepMutex::default(),
}
}
/// Creates a `GpuPowerDomain` wired directly to explicit `PowerElementProvider` and
/// `Topology` channels (used by unit/integration tests).
#[cfg(test)]
fn with_proxies(
provider: fmagma::PowerElementProviderSynchronousProxy,
topology: fpbroker::TopologySynchronousProxy,
) -> Self {
let domain = Self::with_topology(topology);
domain.set_provider(Arc::new(provider));
domain
}
/// Creates a `GpuPowerDomain` wired to an explicit `Topology` channel while waiting for
/// `watch_power_service` to supply `PowerElementProvider` instances (used by unit tests).
#[cfg(test)]
fn with_topology(topology: fpbroker::TopologySynchronousProxy) -> Self {
Self {
config: GpuDeviceConfig::default(),
topology: Some(topology),
state: LockDepMutex::default(),
}
}
/// Installs a newly discovered `PowerElementProvider` proxy from `Service::watch`, queries its
/// `OnReadyForWork` power goal token, and reconciles any active power-on, min-frequency, or
/// max-frequency votes.
fn set_provider(&self, provider: Arc<fmagma::PowerElementProviderSynchronousProxy>) {
let power_on_token = match provider.get_power_goals(fidl_deadline()) {
Ok(goals) => goals
.into_iter()
.find(|g| g.type_ == Some(fmagma::PowerGoalType::OnReadyForWork))
.and_then(|g| g.token),
Err(err) => {
log_warn!("PowerElementProvider::GetPowerGoals failed: {:?}", err);
return;
}
};
let (reconcile_power, reconcile_min_freq, reconcile_max_freq, _dropped_handles) = {
let mut state = self.state.lock();
let dropped = state.clear_provider();
state.provider = Some(provider);
state.power_on_token = power_on_token;
(
state.power_control_on,
!state.min_votes.is_empty(),
!state.max_votes.is_empty(),
dropped,
)
};
if reconcile_power {
let _ = self.sync_power_control();
}
if reconcile_min_freq {
let _ = self.sync_min_freq();
}
if reconcile_max_freq {
let _ = self.sync_max_freq();
}
}
fn acquire_lease(
&self,
token: zx::Event,
level: fpbroker::PowerLevel,
lease_name: &str,
) -> Result<zx::EventPair, ()> {
let Some(topology) = self.topology.as_ref() else {
return Err(());
};
let (client_token, broker_token) = zx::EventPair::create();
let schema = fpbroker::LeaseSchema {
lease_token: Some(broker_token),
lease_name: Some(lease_name.to_string()),
dependencies: Some(vec![fpbroker::LeaseDependency {
requires_token: Some(token),
requires_level: Some(level),
..Default::default()
}]),
// Return the pending lease immediately so Android Power HAL hints (e.g.
// DISPLAY_UPDATE_IMMINENT -> GPUPowerOn) do not block NodeLooperThread on
// cross-process Power Broker -> GPU driver -> google-cpm transitions.
should_return_pending_lease: Some(true),
..Default::default()
};
match topology.lease(schema, fidl_deadline()) {
Ok(Ok(())) => Ok(client_token),
Ok(Err(err)) => {
log_warn!("Power Broker rejected GPU lease {}: {:?}", lease_name, err);
Err(())
}
Err(err) => {
log_warn!("Transport error leasing {}: {:?}", lease_name, err);
Err(())
}
}
}
fn set_power_control(&self, on: bool) -> Result<(), Errno> {
let _dropped_lease = {
let mut state = self.state.lock();
state.power_control_on = on;
state.power_generation = state.power_generation.wrapping_add(1);
if !on { state.active_power_lease.take() } else { None }
};
if on {
self.sync_power_control()?;
}
Ok(())
}
fn sync_power_control(&self) -> Result<(), Errno> {
let (generation, token) = {
let state = self.state.lock();
if !state.power_control_on || state.active_power_lease.is_some() {
return Ok(());
}
if state.provider.is_none() || self.topology.is_none() {
return Ok(());
}
let Some(ref goal_token) = state.power_on_token else {
return Ok(());
};
let Ok(dup_token) = goal_token.duplicate_handle(zx::Rights::SAME_RIGHTS) else {
return error!(EIO);
};
(state.power_generation, dup_token)
};
let lease_result = self.acquire_lease(token, GPU_POWER_LEVEL_ON, "starnix-gpu-power-on");
let mut state = self.state.lock();
if state.power_generation != generation || !state.power_control_on {
drop(state);
return lease_result.map(|_| ()).map_err(|()| errno!(EIO));
}
if let Ok(lease) = lease_result {
let prev = state.active_power_lease.replace(lease);
drop(state);
drop(prev);
return Ok(());
}
drop(state);
error!(EIO)
}
fn power_control_is_on(&self) -> bool {
self.state.lock().power_control_on
}
fn update_min_freq_vote(&self, voter: &'static str, raw_value: u64) -> Result<(), Errno> {
let hz = normalize_gpu_freq_to_hz(raw_value);
let snapped = self.config.snap_min_freq_hz(hz);
{
let mut state = self.state.lock();
match snapped {
Some(freq_hz) => {
state.min_votes.insert(voter, freq_hz);
}
None => {
state.min_votes.remove(voter);
}
}
state.min_freq_generation = state.min_freq_generation.wrapping_add(1);
}
self.sync_min_freq()
}
fn sync_min_freq(&self) -> Result<(), Errno> {
let (generation, target_hz, provider) = {
let mut state = self.state.lock();
if !state.clock_speed_level_supported {
return Ok(());
}
let effective_min_hz = state.min_votes.values().copied().max();
if state.applied_min_freq_hz == effective_min_hz {
return Ok(());
}
let Some(target_hz) = effective_min_hz else {
let dropped = state.min_freq_lease.take();
state.applied_min_freq_hz = None;
drop(state);
drop(dropped);
return Ok(());
};
let (Some(provider), true) =
(state.provider.as_ref().cloned(), self.topology.is_some())
else {
let dropped = state.min_freq_lease.take();
state.applied_min_freq_hz = None;
drop(state);
drop(dropped);
return Ok(());
};
(state.min_freq_generation, target_hz, provider)
};
let req = fmagma::PowerElementProviderGetClockSpeedLevelRequest {
hz: Some(target_hz),
allow_max: Some(true),
..Default::default()
};
// Acquire the new lease outside `self.state.lock()` BEFORE replacing
// `state.min_freq_lease` so Power Broker transitions directly without a transient
// `GPU_POWER_LEVEL_OFF` dip and without holding the state lock across FIDL calls.
let mut reset_provider = false;
let mut not_supported = false;
let mut sync_result = Ok(());
let new_lease = match provider.get_clock_speed_level(&req, fidl_deadline()) {
Ok(Ok(resp)) => {
if let (Some(token), Some(level)) = (resp.token, resp.level) {
match self.acquire_lease(token, level, "starnix-gpu-min-freq") {
Ok(lease) => Some(lease),
Err(()) => {
sync_result = error!(EIO);
None
}
}
} else {
sync_result = error!(EIO);
None
}
}
Ok(Err(status)) => {
let zx_status = zx::Status::err_from_raw(status);
if zx_status == zx::Status::NOT_SUPPORTED {
not_supported = true;
} else {
log_warn!("GetClockSpeedLevel({}) returned status {}", target_hz, zx_status);
sync_result = error!(EIO);
}
None
}
Err(err) => {
log_warn!("GetClockSpeedLevel transport error: {:?}", err);
reset_provider = true;
sync_result = error!(EIO);
None
}
};
let _dropped_handles = {
let mut state = self.state.lock();
let same_provider = state.provider.as_ref().is_some_and(|p| Arc::ptr_eq(p, &provider));
let dropped_existing = if reset_provider && same_provider {
state.clear_provider()
} else if not_supported && same_provider {
state.clock_speed_level_supported = false;
state.applied_min_freq_hz = None;
(None, state.min_freq_lease.take(), None)
} else {
(None, None, None)
};
let dropped_attempt = if !same_provider
|| !state.clock_speed_level_supported
|| state.min_freq_generation != generation
{
new_lease
} else if let Some(lease) = new_lease {
state.applied_min_freq_hz = Some(target_hz);
state.min_freq_lease.replace(lease)
} else if state.applied_min_freq_hz != Some(target_hz) {
state.applied_min_freq_hz = None;
state.min_freq_lease.take()
} else {
None
};
(dropped_existing, dropped_attempt)
};
sync_result
}
fn min_voter_freq_hz(&self, voter: &'static str) -> u64 {
let state = self.state.lock();
if voter == "min_freq" {
state.min_votes.values().copied().max().unwrap_or(0)
} else {
state.min_votes.get(voter).copied().unwrap_or(0)
}
}
fn update_max_freq_vote(&self, voter: &'static str, raw_value: u64) -> Result<(), Errno> {
let hz = normalize_gpu_freq_to_hz(raw_value);
let snapped = self.config.snap_max_freq_hz(hz);
{
let mut state = self.state.lock();
match snapped {
Some(freq_hz) => {
state.max_votes.insert(voter, freq_hz);
}
None => {
state.max_votes.remove(voter);
}
}
state.max_freq_generation = state.max_freq_generation.wrapping_add(1);
}
self.sync_max_freq()
}
fn sync_max_freq(&self) -> Result<(), Errno> {
let (generation, target_hz, provider) = {
let mut state = self.state.lock();
if !state.clock_limit_supported {
return Ok(());
}
let effective_max_hz = state.max_votes.values().copied().min();
if state.applied_max_freq_hz == effective_max_hz {
return Ok(());
}
let Some(target_hz) = effective_max_hz else {
let dropped = state.max_freq_limit_handle.take();
state.applied_max_freq_hz = None;
drop(state);
drop(dropped);
return Ok(());
};
let Some(provider) = state.provider.as_ref().cloned() else {
let dropped = state.max_freq_limit_handle.take();
state.applied_max_freq_hz = None;
drop(state);
drop(dropped);
return Ok(());
};
(state.max_freq_generation, target_hz, provider)
};
let req = fmagma::PowerElementProviderSetClockLimitRequest {
hz: Some(target_hz),
..Default::default()
};
let mut reset_provider = false;
let mut not_supported = false;
let mut sync_result = Ok(());
let new_handle = match provider.set_clock_limit(&req, fidl_deadline()) {
Ok(Ok(resp)) => {
if let Some(handle) = resp.handle {
Some(handle)
} else {
sync_result = error!(EIO);
None
}
}
Ok(Err(status)) => {
let zx_status = zx::Status::err_from_raw(status);
if zx_status == zx::Status::NOT_SUPPORTED {
not_supported = true;
} else {
log_warn!("SetClockLimit({}) returned status {}", target_hz, zx_status);
sync_result = error!(EIO);
}
None
}
Err(err) => {
log_warn!("SetClockLimit transport error: {:?}", err);
reset_provider = true;
sync_result = error!(EIO);
None
}
};
let _dropped_handles = {
let mut state = self.state.lock();
let same_provider = state.provider.as_ref().is_some_and(|p| Arc::ptr_eq(p, &provider));
let dropped_existing = if reset_provider && same_provider {
state.clear_provider()
} else if not_supported && same_provider {
state.clock_limit_supported = false;
state.applied_max_freq_hz = None;
(None, None, state.max_freq_limit_handle.take())
} else {
(None, None, None)
};
let dropped_attempt = if !same_provider
|| !state.clock_limit_supported
|| state.max_freq_generation != generation
{
new_handle
} else if let Some(handle) = new_handle {
state.applied_max_freq_hz = Some(target_hz);
state.max_freq_limit_handle.replace(handle)
} else if state.applied_max_freq_hz != Some(target_hz) {
state.applied_max_freq_hz = None;
state.max_freq_limit_handle.take()
} else {
None
};
(dropped_existing, dropped_attempt)
};
sync_result
}
fn max_voter_freq_hz(&self, voter: &'static str) -> u64 {
let max_hz = self.config.max_freq_hz();
let state = self.state.lock();
if voter == "max_freq" {
state.max_votes.values().copied().min().unwrap_or(max_hz)
} else {
state.max_votes.get(voter).copied().unwrap_or(max_hz)
}
}
fn cur_freq_hz(&self) -> u64 {
let default_hz = self.config.default_freq_hz;
let max_hz = self.config.max_freq_hz();
let state = self.state.lock();
let floor = state.min_votes.values().copied().max().unwrap_or(default_hz);
let ceil = state.max_votes.values().copied().min().unwrap_or(max_hz);
floor.max(default_hz).min(ceil)
}
fn set_polling_interval_ms(&self, ms: u64) {
self.state.lock().polling_interval_ms = ms;
}
fn polling_interval_ms(&self) -> u64 {
self.state.lock().polling_interval_ms
}
}
struct GpuPowerControlFile {
domain: Arc<GpuPowerDomain>,
}
impl GpuPowerControlFile {
fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
BytesFile::new_node(Self { domain })
}
}
impl BytesFileOps for GpuPowerControlFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let s: &[u8] = if self.domain.power_control_is_on() { b"on\n" } else { b"auto\n" };
Ok(s.into())
}
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let s = std::str::from_utf8(&data).map_err(|_| errno!(EINVAL))?.trim();
match s {
"on" => self.domain.set_power_control(true),
"auto" => self.domain.set_power_control(false),
_ => error!(EINVAL),
}
}
}
struct GpuRuntimeStatusFile {
domain: Arc<GpuPowerDomain>,
}
impl GpuRuntimeStatusFile {
fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
BytesFile::new_node(Self { domain })
}
}
impl BytesFileOps for GpuRuntimeStatusFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let s: &[u8] = if self.domain.power_control_is_on() { b"active\n" } else { b"suspended\n" };
Ok(s.into())
}
}
struct GpuFreqVoteFile<const IS_MAX: bool> {
domain: Arc<GpuPowerDomain>,
voter: &'static str,
}
impl<const IS_MAX: bool> GpuFreqVoteFile<IS_MAX> {
fn new_node(domain: Arc<GpuPowerDomain>, voter: &'static str) -> impl FsNodeOps {
BytesFile::new_node(Self { domain, voter })
}
}
impl<const IS_MAX: bool> BytesFileOps for GpuFreqVoteFile<IS_MAX> {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let freq_hz = if IS_MAX {
self.domain.max_voter_freq_hz(self.voter)
} else {
self.domain.min_voter_freq_hz(self.voter)
};
Ok(format!("{freq_hz}\n").into_bytes().into())
}
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let val = parse_unsigned_file::<u64>(&data)?;
if IS_MAX {
self.domain.update_max_freq_vote(self.voter, val)
} else {
self.domain.update_min_freq_vote(self.voter, val)
}
}
}
struct GpuCurFreqFile {
domain: Arc<GpuPowerDomain>,
}
impl GpuCurFreqFile {
fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
BytesFile::new_node(Self { domain })
}
}
impl BytesFileOps for GpuCurFreqFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}\n", self.domain.cur_freq_hz()).into_bytes().into())
}
}
struct GpuPollingIntervalFile {
domain: Arc<GpuPowerDomain>,
}
impl GpuPollingIntervalFile {
fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
BytesFile::new_node(Self { domain })
}
}
impl BytesFileOps for GpuPollingIntervalFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}\n", self.domain.polling_interval_ms()).into_bytes().into())
}
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let ms = parse_unsigned_file::<u64>(&data)?;
self.domain.set_polling_interval_ms(ms);
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::disallowed_types)]
mod tests {
use super::*;
use fidl::endpoints::create_endpoints;
use fidl_fuchsia_io as fio;
use fuchsia_component::server::ServiceFs;
use futures::StreamExt;
use starnix_core::testing::spawn_kernel_and_run;
use starnix_sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
#[::fuchsia::test]
async fn sysfs_registers_gpu_devfreq_and_genpd_nodes() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
register_gpu_devices(kernel, GpuDeviceConfig::default());
let root = &kernel.device_registry.objects.root;
assert!(root.lookup("devices/genpd:0:34f00000.gpu0/power/control".into()).is_some());
assert!(
root.lookup("devices/genpd:0:34f00000.gpu0/power/runtime_status".into()).is_some()
);
assert!(root.lookup("class/devfreq/34f00000.gpu0".into()).is_some());
assert!(root.lookup("bus/platform/devices/34f00000.gpu0".into()).is_some());
assert!(root.lookup("devices/platform/34f00000.gpu0/power/control".into()).is_some());
assert!(
root.lookup(
"devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/vote_manager/powerhint_min_freq"
.into()
)
.is_some()
);
assert!(
root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/min_freq".into())
.is_some()
);
assert!(
root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/max_freq".into())
.is_some()
);
assert!(
root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/cur_freq".into())
.is_some()
);
assert!(
root.lookup(
"devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/available_frequencies"
.into()
)
.is_some()
);
assert!(
root.lookup(
"devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/polling_interval".into()
)
.is_some()
);
})
.await;
}
struct MockPowerBackend {
power_token_koid: zx::Koid,
clock_token_koid: zx::Koid,
power_token: zx::Event,
clock_token: zx::Event,
leased_power_koid: Arc<AtomicU64>,
leased_clock_koid: Arc<AtomicU64>,
leased_clock_level: Arc<AtomicU64>,
capped_clock_hz: Arc<AtomicU64>,
clock_speed_calls: Arc<AtomicU64>,
clock_limit_calls: Arc<AtomicU64>,
active_broker_tokens: Arc<Mutex<Vec<zx::EventPair>>>,
active_limit_tokens: Arc<Mutex<Vec<zx::EventPair>>>,
}
impl MockPowerBackend {
fn new() -> Self {
let power_token = zx::Event::create();
let power_token_koid = power_token.koid().expect("koid");
let clock_token = zx::Event::create();
let clock_token_koid = clock_token.koid().expect("koid");
Self {
power_token_koid,
clock_token_koid,
power_token,
clock_token,
leased_power_koid: Arc::new(AtomicU64::new(0)),
leased_clock_koid: Arc::new(AtomicU64::new(0)),
leased_clock_level: Arc::new(AtomicU64::new(0)),
capped_clock_hz: Arc::new(AtomicU64::new(0)),
clock_speed_calls: Arc::new(AtomicU64::new(0)),
clock_limit_calls: Arc::new(AtomicU64::new(0)),
active_broker_tokens: Arc::new(Mutex::new(Vec::new())),
active_limit_tokens: Arc::new(Mutex::new(Vec::new())),
}
}
fn spawn_provider(
&self,
scope: &fasync::Scope,
mut stream: fmagma::PowerElementProviderRequestStream,
reject_576mhz_not_supported: bool,
) {
let power_token =
self.power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let clock_token =
self.clock_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let capped_clock_recorder = self.capped_clock_hz.clone();
let clock_speed_calls = self.clock_speed_calls.clone();
let clock_limit_calls = self.clock_limit_calls.clone();
let limit_tokens_holder = self.active_limit_tokens.clone();
scope.spawn(async move {
while let Some(Ok(req)) = stream.next().await {
match req {
fmagma::PowerElementProviderRequest::GetPowerGoals { responder } => {
let dup =
power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let _ = responder.send(vec![fmagma::PowerGoal {
type_: Some(fmagma::PowerGoalType::OnReadyForWork),
token: Some(dup),
..Default::default()
}]);
}
fmagma::PowerElementProviderRequest::GetClockSpeedLevel {
payload,
responder,
} => {
clock_speed_calls.fetch_add(1, Ordering::SeqCst);
if reject_576mhz_not_supported && payload.hz == Some(576_000_000) {
let _ = responder.send(Err(zx::Status::NOT_SUPPORTED.into_raw()));
} else {
let dup = clock_token
.duplicate_handle(zx::Rights::SAME_RIGHTS)
.expect("dup");
let level = if payload.hz == Some(576_000_000) { 6u8 } else { 3u8 };
let _ = responder.send(Ok(
fmagma::PowerElementProviderGetClockSpeedLevelResponse {
token: Some(dup),
level: Some(level),
actual_hz: payload.hz,
..Default::default()
},
));
}
}
fmagma::PowerElementProviderRequest::SetClockLimit {
payload,
responder,
} => {
clock_limit_calls.fetch_add(1, Ordering::SeqCst);
if reject_576mhz_not_supported && payload.hz == Some(576_000_000) {
let _ = responder.send(Err(zx::Status::NOT_SUPPORTED.into_raw()));
} else if reject_576mhz_not_supported && payload.hz == Some(396_000_000)
{
let _ = responder.send(Err(zx::Status::INTERNAL.into_raw()));
} else {
let (client_ep, server_ep) = zx::EventPair::create();
capped_clock_recorder
.store(payload.hz.unwrap_or(0), Ordering::SeqCst);
limit_tokens_holder.lock().push(server_ep);
let _ = responder.send(Ok(
fmagma::PowerElementProviderSetClockLimitResponse {
handle: Some(client_ep),
actual_hz: payload.hz,
..Default::default()
},
));
}
}
_ => {}
}
}
});
}
fn spawn_topology(
&self,
scope: &fasync::Scope,
topology_server: fidl::endpoints::ServerEnd<fpbroker::TopologyMarker>,
reject_leases: bool,
) {
let power_token_raw = self.power_token_koid.raw_koid();
let power_koid_recorder = self.leased_power_koid.clone();
let clock_koid_recorder = self.leased_clock_koid.clone();
let clock_level_recorder = self.leased_clock_level.clone();
let tokens_holder = self.active_broker_tokens.clone();
scope.spawn(async move {
let mut stream = topology_server.into_stream();
while let Some(Ok(req)) = stream.next().await {
if let fpbroker::TopologyRequest::Lease { payload, responder } = req {
assert_eq!(payload.should_return_pending_lease, Some(true));
if reject_leases {
let _ = responder.send(Err(fpbroker::LeaseError::Internal));
continue;
}
if let Some(deps) = payload.dependencies {
for dep in deps {
let koid = dep
.requires_token
.as_ref()
.and_then(|t| t.koid().ok())
.map(|k| k.raw_koid())
.unwrap_or(0);
if koid == power_token_raw {
power_koid_recorder.store(koid, Ordering::SeqCst);
} else {
clock_koid_recorder.store(koid, Ordering::SeqCst);
clock_level_recorder.store(
dep.requires_level.unwrap_or(GPU_POWER_LEVEL_OFF) as u64,
Ordering::SeqCst,
);
}
}
}
if let Some(lt) = payload.lease_token {
tokens_holder.lock().push(lt);
}
let _ = responder.send(Ok(()));
}
}
});
}
}
#[::fuchsia::test]
async fn gpu_power_domain_leases_power_broker_on_hints() {
let (provider_client, provider_server) =
create_endpoints::<fmagma::PowerElementProviderMarker>();
let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
let backend = MockPowerBackend::new();
let scope = fasync::Scope::new();
backend.spawn_provider(&scope, provider_server.into_stream(), false);
backend.spawn_topology(&scope, topology_server, false);
let domain = fasync::unblock(move || {
let domain = GpuPowerDomain::with_proxies(
fmagma::PowerElementProviderSynchronousProxy::new(provider_client.into_channel()),
fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
);
// 1. Acquire `GPUPowerOn` ("on") and `GPUMinFreq` ("576000" kHz -> 576 MHz).
domain.set_power_control(true).expect("set power control on");
assert!(domain.power_control_is_on());
domain.update_min_freq_vote("powerhint_min_freq", 576_000).expect("vote 576000 kHz");
assert_eq!(domain.min_voter_freq_hz("min_freq"), 576_000_000);
assert_eq!(domain.cur_freq_hz(), 576_000_000);
// 2. Release votes ("auto" and "0").
domain.update_min_freq_vote("powerhint_min_freq", 0).expect("clear min_freq");
assert_eq!(domain.min_voter_freq_hz("min_freq"), 0);
assert_eq!(domain.cur_freq_hz(), 396_000_000);
domain.update_max_freq_vote("powerhint_max_freq", 300_000).expect("cap max_freq");
assert_eq!(domain.max_voter_freq_hz("max_freq"), 300_000_000);
assert_eq!(domain.cur_freq_hz(), 300_000_000);
domain.update_max_freq_vote("powerhint_max_freq", 0).expect("clear max_freq with 0");
assert_eq!(domain.max_voter_freq_hz("max_freq"), 1_094_000_000);
assert_eq!(domain.cur_freq_hz(), 396_000_000);
domain.set_power_control(false).expect("set power control auto");
assert!(!domain.power_control_is_on());
domain
})
.await;
assert_eq!(
backend.leased_power_koid.load(Ordering::SeqCst),
backend.power_token_koid.raw_koid()
);
assert_eq!(
backend.leased_clock_koid.load(Ordering::SeqCst),
backend.clock_token_koid.raw_koid()
);
assert_eq!(backend.leased_clock_level.load(Ordering::SeqCst), 6);
assert_eq!(backend.capped_clock_hz.load(Ordering::SeqCst), 300_000_000);
// Verify both server-side lease tokens received `EVENTPAIR_PEER_CLOSED` on release!
let broker_tokens = backend.active_broker_tokens.lock();
assert_eq!(broker_tokens.len(), 2);
for bt in broker_tokens.iter() {
let signals = bt
.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST);
assert!(signals.is_ok());
}
// Verify the SetClockLimit handle also received `EVENTPAIR_PEER_CLOSED` when cleared!
let limit_tokens = backend.active_limit_tokens.lock();
assert_eq!(limit_tokens.len(), 1);
for lt in limit_tokens.iter() {
let signals = lt
.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST);
assert!(signals.is_ok());
}
drop(domain);
}
#[::fuchsia::test]
async fn watch_power_service_reconciles_active_votes_on_provider_arrival() {
let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
let backend = Arc::new(MockPowerBackend::new());
let scope = fasync::Scope::new();
backend.spawn_topology(&scope, topology_server, false);
let (dir_client, dir_server) = create_endpoints::<fio::DirectoryMarker>();
let mut fs = ServiceFs::new();
fs.add_fidl_service_instance("default", |req: fmagma::PowerServiceRequest| req);
fs.serve_connection(dir_server).expect("serve PowerService directory");
let backend_for_fs = Arc::clone(&backend);
scope.spawn(async move {
let inner_scope = fasync::Scope::new();
while let Some(fmagma::PowerServiceRequest::PowerElementProvider(stream)) =
fs.next().await
{
backend_for_fs.spawn_provider(&inner_scope, stream, false);
}
});
let domain = Arc::new(GpuPowerDomain::with_topology(
fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
));
// Record power and min-frequency votes BEFORE any `PowerElementProvider` is connected.
domain.set_power_control(true).expect("power control before provider");
domain.update_min_freq_vote("powerhint_min_freq", 576_000).expect("vote before provider");
assert_eq!(backend.leased_power_koid.load(Ordering::SeqCst), 0);
assert_eq!(backend.leased_clock_koid.load(Ordering::SeqCst), 0);
// Now start `watch_power_service` against the ServiceFs directory and verify both active
// votes are automatically reconciled as soon as the instance is discovered.
let service =
fclient::Service::open_from_dir(dir_client, fmagma::PowerServiceMarker).expect("open");
let watcher_domain = Arc::clone(&domain);
scope.spawn(async move {
let _ = watch_power_service(watcher_domain, service).await;
});
while backend.leased_power_koid.load(Ordering::SeqCst) == 0
|| backend.leased_clock_koid.load(Ordering::SeqCst) == 0
{
fasync::Timer::new(zx::MonotonicDuration::from_millis(10)).await;
}
assert_eq!(
backend.leased_power_koid.load(Ordering::SeqCst),
backend.power_token_koid.raw_koid()
);
assert_eq!(
backend.leased_clock_koid.load(Ordering::SeqCst),
backend.clock_token_koid.raw_koid()
);
assert_eq!(backend.leased_clock_level.load(Ordering::SeqCst), 6);
}
#[::fuchsia::test]
async fn gpu_power_domain_returns_eio_on_lease_error_and_allows_not_supported() {
let (provider_client, provider_server) =
create_endpoints::<fmagma::PowerElementProviderMarker>();
let (provider2_client, provider2_server) =
create_endpoints::<fmagma::PowerElementProviderMarker>();
let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
let backend = MockPowerBackend::new();
let scope = fasync::Scope::new();
backend.spawn_provider(&scope, provider_server.into_stream(), true);
backend.spawn_provider(&scope, provider2_server.into_stream(), false);
backend.spawn_topology(&scope, topology_server, true);
let domain = fasync::unblock(move || {
let domain = GpuPowerDomain::with_proxies(
fmagma::PowerElementProviderSynchronousProxy::new(provider_client.into_channel()),
fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
);
// 1. Topology::Lease rejection on `GPUPowerOn` returns EIO.
assert_eq!(domain.set_power_control(true), error!(EIO));
// 2. Topology::Lease rejection on `GetClockSpeedLevel` returns EIO.
assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 396_000), error!(EIO));
// 3. `ZX_ERR_NOT_SUPPORTED` from `GetClockSpeedLevel` is gracefully ignored (Ok(()))
// and latches `clock_speed_level_supported = false` so subsequent votes (including
// after dropping to 0 Hz or switching target frequencies) do not make additional
// FIDL calls.
assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 576_000), Ok(()));
assert_eq!(domain.update_min_freq_vote("soft_min_freq", 576_000), Ok(()));
assert_eq!(domain.update_min_freq_vote("soft_min_freq", 0), Ok(()));
assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 0), Ok(()));
assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 576_000), Ok(()));
assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 396_000), Ok(()));
// 4. Non-`NOT_SUPPORTED` errors from `SetClockLimit` propagate EIO, while
// `ZX_ERR_NOT_SUPPORTED` is gracefully ignored (Ok(())) and latches
// `clock_limit_supported = false` across subsequent transitions (including dropping
// to 0 Hz so `max_votes` becomes empty).
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 396_000), error!(EIO));
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 576_000), Ok(()));
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 0), Ok(()));
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 972_000), Ok(()));
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 576_000), Ok(()));
assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 396_000), Ok(()));
domain
})
.await;
// 396_000_000 was queried once and 576_000_000 was queried once on each method before
// latching `clock_speed_level_supported = false` and `clock_limit_supported = false`.
assert_eq!(backend.clock_speed_calls.load(Ordering::SeqCst), 2);
assert_eq!(backend.clock_limit_calls.load(Ordering::SeqCst), 2);
assert_eq!(backend.capped_clock_hz.load(Ordering::SeqCst), 0);
// 5. Installing a new `PowerElementProvider` via `set_provider` resets both supported
// flags and reconciles the active 396 MHz min and max votes against the new provider.
let domain = fasync::unblock(move || {
domain.set_provider(Arc::new(fmagma::PowerElementProviderSynchronousProxy::new(
provider2_client.into_channel(),
)));
domain
})
.await;
assert_eq!(backend.clock_speed_calls.load(Ordering::SeqCst), 3);
assert_eq!(backend.clock_limit_calls.load(Ordering::SeqCst), 3);
assert_eq!(backend.capped_clock_hz.load(Ordering::SeqCst), 396_000_000);
drop(domain);
}
#[::fuchsia::test]
async fn concurrent_not_supported_and_provider_reset_do_not_leak_handles() {
let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
let backend = MockPowerBackend::new();
let scope = fasync::Scope::new();
backend.spawn_topology(&scope, topology_server, false);
let domain = Arc::new(GpuPowerDomain::with_topology(
fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
));
// 1. Concurrent `NOT_SUPPORTED` (older generation) + `Ok` (newer generation) must drop
// the newer lease/handle rather than installing it after `clock_*_supported` latches to
// `false`.
let (provider1_client, provider1_server) =
create_endpoints::<fmagma::PowerElementProviderMarker>();
let mut stream1 = provider1_server.into_stream();
let power_dup = backend.power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let domain_for_init = Arc::clone(&domain);
let init_fut = fasync::unblock(move || {
domain_for_init.set_provider(Arc::new(
fmagma::PowerElementProviderSynchronousProxy::new(provider1_client.into_channel()),
));
});
match stream1.next().await.expect("GetPowerGoals").expect("fidl") {
fmagma::PowerElementProviderRequest::GetPowerGoals { responder } => {
responder
.send(vec![fmagma::PowerGoal {
type_: Some(fmagma::PowerGoalType::OnReadyForWork),
token: Some(power_dup),
..Default::default()
}])
.expect("reply GetPowerGoals");
}
other => panic!("unexpected request: {other:?}"),
}
init_fut.await;
let d1 = Arc::clone(&domain);
let min_v1 = fasync::unblock(move || d1.update_min_freq_vote("v1", 576_000));
let min_resp1 = match stream1.next().await.expect("min req1").expect("fidl") {
fmagma::PowerElementProviderRequest::GetClockSpeedLevel { payload, responder } => {
assert_eq!(payload.hz, Some(576_000_000));
responder
}
other => panic!("unexpected request: {other:?}"),
};
let d2 = Arc::clone(&domain);
let min_v2 = fasync::unblock(move || d2.update_min_freq_vote("v2", 921_000));
let min_resp2 = match stream1.next().await.expect("min req2").expect("fidl") {
fmagma::PowerElementProviderRequest::GetClockSpeedLevel { payload, responder } => {
assert_eq!(payload.hz, Some(921_000_000));
responder
}
other => panic!("unexpected request: {other:?}"),
};
// Older generation latches `clock_speed_level_supported = false` first; newer generation
// then succeeds in FIDL + Power Broker lease acquisition, and must drop `new_lease`.
min_resp1.send(Err(zx::Status::NOT_SUPPORTED.into_raw())).expect("send NOT_SUPPORTED");
assert_eq!(min_v1.await, Ok(()));
let clock_dup = backend.clock_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
min_resp2
.send(Ok(fmagma::PowerElementProviderGetClockSpeedLevelResponse {
token: Some(clock_dup),
level: Some(6),
actual_hz: Some(921_000_000),
..Default::default()
}))
.expect("send Ok");
assert_eq!(min_v2.await, Ok(()));
{
let state = domain.state.lock();
assert!(!state.clock_speed_level_supported);
assert!(state.min_freq_lease.is_none());
assert!(state.applied_min_freq_hz.is_none());
}
{
let broker_tokens = backend.active_broker_tokens.lock();
assert_eq!(broker_tokens.len(), 1);
assert!(
broker_tokens[0]
.wait_one(
zx::Signals::EVENTPAIR_PEER_CLOSED,
zx::MonotonicInstant::INFINITE_PAST
)
.is_ok()
);
}
// Same interleaving on `SetClockLimit`.
let d1 = Arc::clone(&domain);
let max_v1 = fasync::unblock(move || d1.update_max_freq_vote("v1", 576_000));
let max_resp1 = match stream1.next().await.expect("max req1").expect("fidl") {
fmagma::PowerElementProviderRequest::SetClockLimit { payload, responder } => {
assert_eq!(payload.hz, Some(576_000_000));
responder
}
other => panic!("unexpected request: {other:?}"),
};
let d2 = Arc::clone(&domain);
let max_v2 = fasync::unblock(move || d2.update_max_freq_vote("v2", 396_000));
let max_resp2 = match stream1.next().await.expect("max req2").expect("fidl") {
fmagma::PowerElementProviderRequest::SetClockLimit { payload, responder } => {
assert_eq!(payload.hz, Some(396_000_000));
responder
}
other => panic!("unexpected request: {other:?}"),
};
max_resp1.send(Err(zx::Status::NOT_SUPPORTED.into_raw())).expect("send NOT_SUPPORTED");
assert_eq!(max_v1.await, Ok(()));
let (limit_client_ep, limit_server_ep) = zx::EventPair::create();
max_resp2
.send(Ok(fmagma::PowerElementProviderSetClockLimitResponse {
handle: Some(limit_client_ep),
actual_hz: Some(396_000_000),
..Default::default()
}))
.expect("send Ok");
assert_eq!(max_v2.await, Ok(()));
{
let state = domain.state.lock();
assert!(!state.clock_limit_supported);
assert!(state.max_freq_limit_handle.is_none());
assert!(state.applied_max_freq_hz.is_none());
}
assert!(
limit_server_ep
.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST)
.is_ok()
);
drop(stream1);
// Reset votes before testing provider transport-error cleanup.
assert_eq!(domain.update_min_freq_vote("v1", 0), Ok(()));
assert_eq!(domain.update_min_freq_vote("v2", 0), Ok(()));
assert_eq!(domain.update_max_freq_vote("v1", 0), Ok(()));
assert_eq!(domain.update_max_freq_vote("v2", 0), Ok(()));
// 2. Transport error (`reset_provider`) when `min_freq_generation` advanced while the lock
// was dropped must still clear `provider`, `applied_*_freq_hz`, and all active provider
// leases/handles.
let (provider2_client, provider2_server) =
create_endpoints::<fmagma::PowerElementProviderMarker>();
let mut stream2 = provider2_server.into_stream();
let (active_limit_client, active_limit_server) = zx::EventPair::create();
let power_dup2 =
backend.power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let clock_dup2 =
backend.clock_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
let domain_setup = Arc::clone(&domain);
let setup_fut = fasync::unblock(move || {
domain_setup.set_provider(Arc::new(fmagma::PowerElementProviderSynchronousProxy::new(
provider2_client.into_channel(),
)));
domain_setup.set_power_control(true).expect("power on");
domain_setup.update_min_freq_vote("base", 396_000).expect("min 396");
domain_setup.update_max_freq_vote("base", 972_000).expect("max 972");
});
match stream2.next().await.expect("GetPowerGoals").expect("fidl") {
fmagma::PowerElementProviderRequest::GetPowerGoals { responder } => {
responder
.send(vec![fmagma::PowerGoal {
type_: Some(fmagma::PowerGoalType::OnReadyForWork),
token: Some(power_dup2),
..Default::default()
}])
.expect("reply GetPowerGoals");
}
other => panic!("unexpected request: {other:?}"),
}
match stream2.next().await.expect("GetClockSpeedLevel").expect("fidl") {
fmagma::PowerElementProviderRequest::GetClockSpeedLevel { responder, .. } => {
responder
.send(Ok(fmagma::PowerElementProviderGetClockSpeedLevelResponse {
token: Some(clock_dup2),
level: Some(3),
actual_hz: Some(396_000_000),
..Default::default()
}))
.expect("reply GetClockSpeedLevel");
}
other => panic!("unexpected request: {other:?}"),
}
match stream2.next().await.expect("SetClockLimit").expect("fidl") {
fmagma::PowerElementProviderRequest::SetClockLimit { responder, .. } => {
responder
.send(Ok(fmagma::PowerElementProviderSetClockLimitResponse {
handle: Some(active_limit_client),
actual_hz: Some(921_000_000),
..Default::default()
}))
.expect("reply SetClockLimit");
}
other => panic!("unexpected request: {other:?}"),
}
setup_fut.await;
{
let state = domain.state.lock();
assert!(state.active_power_lease.is_some());
assert!(state.min_freq_lease.is_some());
assert!(state.max_freq_limit_handle.is_some());
}
// Start an in-flight `GetClockSpeedLevel(576 MHz)`, then advance `min_freq_generation`
// back to the already-applied 396 MHz while the lock is dropped, then close the provider
// channel so the in-flight call hits `PEER_CLOSED` (`reset_provider = true`).
let d_err = Arc::clone(&domain);
let err_fut = fasync::unblock(move || d_err.update_min_freq_vote("boost", 576_000));
let pending_resp = match stream2.next().await.expect("boost req").expect("fidl") {
fmagma::PowerElementProviderRequest::GetClockSpeedLevel { payload, responder } => {
assert_eq!(payload.hz, Some(576_000_000));
responder
}
other => panic!("unexpected request: {other:?}"),
};
assert_eq!(domain.update_min_freq_vote("boost", 0), Ok(()));
drop(pending_resp);
drop(stream2);
assert_eq!(err_fut.await, error!(EIO));
{
let state = domain.state.lock();
assert!(state.provider.is_none());
assert!(state.applied_min_freq_hz.is_none());
assert!(state.applied_max_freq_hz.is_none());
assert!(state.active_power_lease.is_none());
assert!(state.min_freq_lease.is_none());
assert!(state.max_freq_limit_handle.is_none());
}
assert!(
active_limit_server
.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST)
.is_ok()
);
for bt in backend.active_broker_tokens.lock().iter() {
assert!(
bt.wait_one(
zx::Signals::EVENTPAIR_PEER_CLOSED,
zx::MonotonicInstant::INFINITE_PAST
)
.is_ok()
);
}
}
}