Skip to main content

starnix_modules_magma/
gpu_class_directory.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Context as _;
6use fidl_fuchsia_gpu_magma as fmagma;
7use fidl_fuchsia_power_broker as fpbroker;
8use fuchsia_async as fasync;
9use fuchsia_component::client::{self as fclient, connect_to_protocol_sync};
10use futures::TryStreamExt;
11use itertools::Itertools;
12use starnix_core::fs::sysfs::build_device_directory;
13use starnix_core::task::{CurrentTask, Kernel};
14use starnix_core::vfs::FsNodeOps;
15use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
16use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps, parse_unsigned_file};
17use starnix_logging::{log_info, log_warn};
18use starnix_sync::{LockDepMutex, MagmaPowerDomainLock};
19use starnix_uapi::errors::Errno;
20use starnix_uapi::file_mode::mode;
21use starnix_uapi::{errno, error};
22use std::borrow::Cow;
23use std::collections::HashMap;
24use std::sync::Arc;
25
26/// Sysfs name of the PowerVR GPU device (`0x34f00000`).
27const DEFAULT_GPU_DEVICE_NAME: &str = "34f00000.gpu0";
28
29/// Sysfs name of the GPU runtime PM genpd device (`GPUPowerOn`).
30const DEFAULT_GPU_GENPD_DEVICE_NAME: &str = "genpd:0:34f00000.gpu0";
31
32/// Maximum time to wait for synchronous FIDL calls to the Magma driver or Power Broker.
33const FIDL_CALL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
34
35fn fidl_deadline() -> zx::MonotonicInstant {
36    zx::MonotonicInstant::after(FIDL_CALL_TIMEOUT)
37}
38
39// TODO(https://fxbug.dev/564595867): Query the operating point frequency table from
40// `fuchsia.gpu.magma.PowerElementProvider` once a frequency-table FIDL method is added.
41/// 15-state GPU operating point frequency table sorted ascending in Hz (`PF14` .. `PF0`).
42const DEFAULT_GPU_AVAILABLE_FREQUENCIES_HZ: &[u64] = &[
43    198_000_000,   // PF14
44    253_000_000,   // PF13
45    300_000_000,   // PF12
46    396_000_000,   // PF11 (boot default)
47    448_000_000,   // PF10
48    512_000_000,   // PF9
49    576_000_000,   // PF8  (LAUNCH / EXPENSIVE_RENDERING)
50    633_000_000,   // PF7
51    691_000_000,   // PF6
52    748_000_000,   // PF5
53    806_000_000,   // PF4
54    864_000_000,   // PF3
55    921_000_000,   // PF2
56    998_000_000,   // PF1
57    1_094_000_000, // PF0
58];
59
60const DEFAULT_GPU_FREQ_HZ: u64 = 396_000_000;
61const DEFAULT_POLLING_INTERVAL_MS: u64 = 20;
62
63/// Power Broker level when the GPU is powered off (`OFF`).
64#[cfg_attr(not(test), allow(dead_code))]
65const GPU_POWER_LEVEL_OFF: fpbroker::PowerLevel = 0;
66
67/// Power Broker level when the GPU is powered on and ready for work (`ON_READY_FOR_WORK`).
68const GPU_POWER_LEVEL_ON: fpbroker::PowerLevel = 1;
69
70/// Container-provided configuration for GPU sysfs device registration and operating point
71/// frequencies.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct GpuDeviceConfig {
74    /// Linux platform/devfreq sysfs device name (e.g. `"34f00000.gpu0"`).
75    pub device_name: String,
76
77    /// Linux runtime PM genpd sysfs device name (e.g. `"genpd:0:34f00000.gpu0"`).
78    pub genpd_device_name: String,
79
80    // TODO(https://fxbug.dev/564595867): Query `available_frequencies_hz` from
81    // `fuchsia.gpu.magma.PowerElementProvider` once a frequency-table FIDL method is added.
82    /// Operating point frequencies sorted ascending in Hz.
83    pub available_frequencies_hz: Vec<u64>,
84
85    /// Default / baseline GPU clock frequency in Hz when no floor vote exceeds it.
86    pub default_freq_hz: u64,
87}
88
89impl Default for GpuDeviceConfig {
90    fn default() -> Self {
91        Self {
92            device_name: DEFAULT_GPU_DEVICE_NAME.to_string(),
93            genpd_device_name: DEFAULT_GPU_GENPD_DEVICE_NAME.to_string(),
94            available_frequencies_hz: DEFAULT_GPU_AVAILABLE_FREQUENCIES_HZ.to_vec(),
95            default_freq_hz: DEFAULT_GPU_FREQ_HZ,
96        }
97    }
98}
99
100impl GpuDeviceConfig {
101    fn min_freq_hz(&self) -> u64 {
102        self.available_frequencies_hz.first().copied().unwrap_or(self.default_freq_hz)
103    }
104
105    fn max_freq_hz(&self) -> u64 {
106        self.available_frequencies_hz.last().copied().unwrap_or(self.default_freq_hz)
107    }
108
109    /// Snaps a floor vote in Hz up to the smallest GPU operating point `>= hz` (clamped to the
110    /// maximum operating point).
111    fn snap_min_freq_hz(&self, hz: u64) -> Option<u64> {
112        if hz == 0 {
113            return None;
114        }
115        Some(
116            self.available_frequencies_hz
117                .iter()
118                .copied()
119                .find(|&opp_hz| opp_hz >= hz)
120                .unwrap_or_else(|| self.max_freq_hz()),
121        )
122    }
123
124    /// Snaps a ceiling vote in Hz down to the highest GPU operating point `<= hz` (or `None` when
125    /// `hz == 0` or unconstrained at or above the maximum operating point).
126    fn snap_max_freq_hz(&self, hz: u64) -> Option<u64> {
127        let max_hz = self.max_freq_hz();
128        if hz == 0 || hz >= max_hz {
129            return None;
130        }
131        Some(
132            self.available_frequencies_hz
133                .iter()
134                .rev()
135                .copied()
136                .find(|&opp_hz| opp_hz <= hz)
137                .unwrap_or_else(|| self.min_freq_hz()),
138        )
139    }
140}
141
142/// Threshold separating kHz values (e.g. `396000` / `576000` in kHz-based `powerhint.json`) from
143/// Hz values (e.g. `396000000` / `576000000` in Linux `devfreq` and Hz-based `powerhint.json`).
144const KHZ_TO_HZ_THRESHOLD: u64 = 10_000_000;
145
146const VOTE_MANAGER_MIN_FREQ_NODES: &[&str] =
147    &["powerhint_min_freq", "soft_min_freq", "debug_min_freq"];
148
149const VOTE_MANAGER_MAX_FREQ_NODES: &[&str] =
150    &["powerhint_max_freq", "thermal_max_freq", "soft_max_freq", "debug_max_freq"];
151
152fn spawn_power_service_watcher(kernel: &Kernel, domain: Arc<GpuPowerDomain>) {
153    kernel.kthreads.spawn_future(
154        move || async move {
155            let service = match fclient::Service::open(fmagma::PowerServiceMarker) {
156                Ok(service) => service,
157                Err(err) => {
158                    log_info!("GPU PowerService directory unavailable: {err:#}");
159                    return;
160                }
161            };
162            if let Err(err) = watch_power_service(domain, service).await {
163                log_info!("GPU PowerService watcher exited: {err:#}");
164            }
165        },
166        "gpu_power_service_watcher",
167    );
168}
169
170/// Watches `service` (`fuchsia.gpu.magma.PowerService`) for provider instances and installs each
171/// new `PowerElementProvider` proxy into `domain`, reconciling any active power or frequency votes.
172async fn watch_power_service(
173    domain: Arc<GpuPowerDomain>,
174    service: fclient::Service<fmagma::PowerServiceMarker>,
175) -> Result<(), anyhow::Error> {
176    let mut instances = service.watch().await.context("failed to watch GPU PowerService")?;
177    while let Some(instance) = instances.try_next().await? {
178        let name = instance.instance_name().to_string();
179        let provider = match instance.connect_to_power_element_provider_sync() {
180            Ok(provider) => Arc::new(provider),
181            Err(err) => {
182                log_warn!("Failed to connect to GPU PowerElementProvider {name}: {err:#}");
183                continue;
184            }
185        };
186        let domain_clone = Arc::clone(&domain);
187        fasync::unblock(move || {
188            domain_clone.set_provider(provider);
189        })
190        .await;
191    }
192    Ok(())
193}
194
195/// Registers GPU devfreq (`/sys/class/devfreq/<device_name>` and
196/// `/sys/devices/platform/<device_name>/devfreq/<device_name>`) and genpd runtime PM
197/// (`/sys/devices/<genpd_device_name>/power/control`) sysfs nodes.
198pub(crate) fn register_gpu_devices(kernel: &Kernel, config: GpuDeviceConfig) {
199    let device_name = config.device_name.clone();
200    let genpd_device_name = config.genpd_device_name.clone();
201    let domain = Arc::new(GpuPowerDomain::new(config));
202    spawn_power_service_watcher(kernel, Arc::clone(&domain));
203    let registry = &kernel.device_registry;
204    let objects = &registry.objects;
205
206    // 1. `/sys/devices/<genpd_device_name>/power/control` (`GPUPowerOn`)
207    let domain_for_genpd = domain.clone();
208    registry.add_subsystemless_device(
209        genpd_device_name.as_str().into(),
210        /* parent = */ None,
211        move |device, dir| {
212            build_device_directory(device, dir);
213            build_gpu_power_directory(dir, &domain_for_genpd);
214        },
215    );
216
217    // 2. `/sys/devices/platform/<device_name>` + `/sys/class/devfreq/<device_name>` (`GPUMinFreq`)
218    let domain_for_platform = domain.clone();
219    let gpu_platform_device = registry.add_bus_device(
220        device_name.as_str().into(),
221        Some(objects.platform_device()),
222        objects.platform_bus(),
223        move |device, dir| {
224            build_device_directory(device, dir);
225            build_gpu_power_directory(dir, &domain_for_platform);
226        },
227    );
228
229    let devfreq_class = objects.get_or_create_class("devfreq".into());
230    registry.add_numberless_device(
231        device_name.as_str().into(),
232        Some(gpu_platform_device),
233        devfreq_class,
234        move |device, dir| {
235            build_device_directory(device, dir);
236            build_gpu_devfreq_directory(dir, &domain);
237        },
238    );
239}
240
241fn build_gpu_power_directory(dir: &SimpleDirectoryMutator, domain: &Arc<GpuPowerDomain>) {
242    dir.subdir("power", 0o755, |dir| {
243        dir.entry("control", GpuPowerControlFile::new_node(domain.clone()), mode!(IFREG, 0o644));
244        dir.entry(
245            "runtime_status",
246            GpuRuntimeStatusFile::new_node(domain.clone()),
247            mode!(IFREG, 0o444),
248        );
249    });
250}
251
252fn build_gpu_devfreq_directory(dir: &SimpleDirectoryMutator, domain: &Arc<GpuPowerDomain>) {
253    dir.entry(
254        "available_frequencies",
255        BytesFile::new_node(
256            (domain.config.available_frequencies_hz.iter().join(" ") + "\n").into_bytes(),
257        ),
258        mode!(IFREG, 0o444),
259    );
260    dir.entry("cur_freq", GpuCurFreqFile::new_node(domain.clone()), mode!(IFREG, 0o444));
261    dir.entry(
262        "min_freq",
263        GpuFreqVoteFile::<false>::new_node(domain.clone(), "min_freq"),
264        mode!(IFREG, 0o644),
265    );
266    dir.entry(
267        "max_freq",
268        GpuFreqVoteFile::<true>::new_node(domain.clone(), "max_freq"),
269        mode!(IFREG, 0o644),
270    );
271    dir.entry(
272        "polling_interval",
273        GpuPollingIntervalFile::new_node(domain.clone()),
274        mode!(IFREG, 0o644),
275    );
276    dir.entry("governor", BytesFile::new_node(b"simple_ondemand\n".to_vec()), mode!(IFREG, 0o444));
277
278    dir.subdir("vote_manager", 0o755, |dir| {
279        for &voter in VOTE_MANAGER_MIN_FREQ_NODES {
280            dir.entry(
281                voter,
282                GpuFreqVoteFile::<false>::new_node(domain.clone(), voter),
283                mode!(IFREG, 0o644),
284            );
285        }
286        for &voter in VOTE_MANAGER_MAX_FREQ_NODES {
287            dir.entry(
288                voter,
289                GpuFreqVoteFile::<true>::new_node(domain.clone(), voter),
290                mode!(IFREG, 0o644),
291            );
292        }
293    });
294}
295
296/// Normalizes a GPU frequency input written in either Hz (`>= 10_000_000`, e.g. `396000000`) or
297/// kHz (`< 10_000_000`, e.g. `396000` / `576000`) to Hz.
298fn normalize_gpu_freq_to_hz(raw_value: u64) -> u64 {
299    if raw_value == 0 {
300        0
301    } else if raw_value < KHZ_TO_HZ_THRESHOLD {
302        raw_value.saturating_mul(1_000)
303    } else {
304        raw_value
305    }
306}
307
308#[derive(Debug)]
309struct GpuPowerDomainState {
310    /// Whether `GPUPowerOn` (`/sys/devices/genpd:0:34f00000.gpu0/power/control`) is set to `"on"`.
311    power_control_on: bool,
312
313    /// Monotonic generation counter for `power_control_on` updates.
314    power_generation: u64,
315
316    /// Active floor votes (`voter -> snapped_hz`). Effective floor is `max(min_votes.values())`.
317    min_votes: HashMap<&'static str, u64>,
318
319    /// Monotonic generation counter for `min_votes` updates.
320    min_freq_generation: u64,
321
322    /// Active ceiling votes (`voter -> snapped_hz`). Effective ceiling is `min(max_votes.values())`.
323    max_votes: HashMap<&'static str, u64>,
324
325    /// Monotonic generation counter for `max_votes` updates.
326    max_freq_generation: u64,
327
328    /// Configured devfreq polling interval in ms.
329    polling_interval_ms: u64,
330
331    /// FIDL proxy to the Magma driver populated by `watch_power_service` (or injected in tests).
332    provider: Option<Arc<fmagma::PowerElementProviderSynchronousProxy>>,
333
334    /// Cached `PowerGoalType::OnReadyForWork` `DependencyToken` fetched when `provider` connects.
335    power_on_token: Option<zx::Event>,
336
337    /// Active Power Broker lease token keeping `gpu_power_on` at `GPU_POWER_LEVEL_ON`
338    /// (`ON_READY_FOR_WORK`).
339    active_power_lease: Option<zx::EventPair>,
340
341    /// Active Power Broker lease token holding `gpu_min_freq` at the highest active floor level.
342    min_freq_lease: Option<zx::EventPair>,
343
344    /// Last effective min-frequency floor (in Hz) synced to Power Broker.
345    applied_min_freq_hz: Option<u64>,
346
347    /// Active `PowerElementProvider::SetClockLimit` handle capping the GPU clock at the lowest
348    /// active ceiling vote.
349    max_freq_limit_handle: Option<zx::EventPair>,
350
351    /// Last effective max-frequency ceiling (in Hz) synced to `PowerElementProvider::SetClockLimit`.
352    applied_max_freq_hz: Option<u64>,
353}
354
355impl Default for GpuPowerDomainState {
356    fn default() -> Self {
357        Self {
358            power_control_on: false,
359            power_generation: 0,
360            min_votes: HashMap::new(),
361            min_freq_generation: 0,
362            max_votes: HashMap::new(),
363            max_freq_generation: 0,
364            polling_interval_ms: DEFAULT_POLLING_INTERVAL_MS,
365            provider: None,
366            power_on_token: None,
367            active_power_lease: None,
368            min_freq_lease: None,
369            applied_min_freq_hz: None,
370            max_freq_limit_handle: None,
371            applied_max_freq_hz: None,
372        }
373    }
374}
375
376/// Coordinates GPU power state and devfreq frequency votes across Starnix sysfs nodes.
377///
378/// Aggregates floor (`min_freq`) and ceiling (`max_freq`) votes from `devfreq` and `vote_manager`
379/// nodes alongside runtime PM (`power/control`) state, and synchronizes the effective power and
380/// clock levels with the Magma driver (`fuchsia.gpu.magma.PowerElementProvider`) and Fuchsia Power
381/// Broker (`fuchsia.power.broker.Topology`).
382#[derive(Debug, Default)]
383struct GpuPowerDomain {
384    config: GpuDeviceConfig,
385    topology: Option<fpbroker::TopologySynchronousProxy>,
386    state: LockDepMutex<GpuPowerDomainState, MagmaPowerDomainLock>,
387}
388
389impl GpuPowerDomain {
390    fn new(config: GpuDeviceConfig) -> Self {
391        Self {
392            config,
393            topology: connect_to_protocol_sync::<fpbroker::TopologyMarker>().ok(),
394            state: LockDepMutex::default(),
395        }
396    }
397
398    /// Creates a `GpuPowerDomain` wired directly to explicit `PowerElementProvider` and
399    /// `Topology` channels (used by unit/integration tests).
400    #[cfg(test)]
401    fn with_proxies(
402        provider: fmagma::PowerElementProviderSynchronousProxy,
403        topology: fpbroker::TopologySynchronousProxy,
404    ) -> Self {
405        let domain = Self::with_topology(topology);
406        domain.set_provider(Arc::new(provider));
407        domain
408    }
409
410    /// Creates a `GpuPowerDomain` wired to an explicit `Topology` channel while waiting for
411    /// `watch_power_service` to supply `PowerElementProvider` instances (used by unit tests).
412    #[cfg(test)]
413    fn with_topology(topology: fpbroker::TopologySynchronousProxy) -> Self {
414        Self {
415            config: GpuDeviceConfig::default(),
416            topology: Some(topology),
417            state: LockDepMutex::default(),
418        }
419    }
420
421    /// Installs a newly discovered `PowerElementProvider` proxy from `Service::watch`, queries its
422    /// `OnReadyForWork` power goal token, and reconciles any active power-on, min-frequency, or
423    /// max-frequency votes.
424    fn set_provider(&self, provider: Arc<fmagma::PowerElementProviderSynchronousProxy>) {
425        let power_on_token = match provider.get_power_goals(fidl_deadline()) {
426            Ok(goals) => goals
427                .into_iter()
428                .find(|g| g.type_ == Some(fmagma::PowerGoalType::OnReadyForWork))
429                .and_then(|g| g.token),
430            Err(err) => {
431                log_warn!("PowerElementProvider::GetPowerGoals failed: {:?}", err);
432                return;
433            }
434        };
435
436        let (
437            reconcile_power,
438            reconcile_min_freq,
439            reconcile_max_freq,
440            _dropped_power_lease,
441            _dropped_min_freq_lease,
442            _dropped_max_freq_handle,
443        ) = {
444            let mut state = self.state.lock();
445            state.provider = Some(provider);
446            state.power_on_token = power_on_token;
447            let power_lease = state.active_power_lease.take();
448            let min_freq_lease = state.min_freq_lease.take();
449            let max_freq_handle = state.max_freq_limit_handle.take();
450            state.applied_min_freq_hz = None;
451            state.applied_max_freq_hz = None;
452            state.power_generation = state.power_generation.wrapping_add(1);
453            state.min_freq_generation = state.min_freq_generation.wrapping_add(1);
454            state.max_freq_generation = state.max_freq_generation.wrapping_add(1);
455            (
456                state.power_control_on,
457                !state.min_votes.is_empty(),
458                !state.max_votes.is_empty(),
459                power_lease,
460                min_freq_lease,
461                max_freq_handle,
462            )
463        };
464        if reconcile_power {
465            let _ = self.sync_power_control();
466        }
467        if reconcile_min_freq {
468            let _ = self.sync_min_freq();
469        }
470        if reconcile_max_freq {
471            let _ = self.sync_max_freq();
472        }
473    }
474
475    fn acquire_lease(
476        &self,
477        token: zx::Event,
478        level: fpbroker::PowerLevel,
479        lease_name: &str,
480    ) -> Result<zx::EventPair, ()> {
481        let Some(topology) = self.topology.as_ref() else {
482            return Err(());
483        };
484        let (client_token, broker_token) = zx::EventPair::create();
485        let schema = fpbroker::LeaseSchema {
486            lease_token: Some(broker_token),
487            lease_name: Some(lease_name.to_string()),
488            dependencies: Some(vec![fpbroker::LeaseDependency {
489                requires_token: Some(token),
490                requires_level: Some(level),
491                ..Default::default()
492            }]),
493            // Return the pending lease immediately so Android Power HAL hints (e.g.
494            // DISPLAY_UPDATE_IMMINENT -> GPUPowerOn) do not block NodeLooperThread on
495            // cross-process Power Broker -> GPU driver -> google-cpm transitions.
496            should_return_pending_lease: Some(true),
497            ..Default::default()
498        };
499        match topology.lease(schema, fidl_deadline()) {
500            Ok(Ok(())) => Ok(client_token),
501            Ok(Err(err)) => {
502                log_warn!("Power Broker rejected GPU lease {}: {:?}", lease_name, err);
503                Err(())
504            }
505            Err(err) => {
506                log_warn!("Transport error leasing {}: {:?}", lease_name, err);
507                Err(())
508            }
509        }
510    }
511
512    fn set_power_control(&self, on: bool) -> Result<(), Errno> {
513        let _dropped_lease = {
514            let mut state = self.state.lock();
515            state.power_control_on = on;
516            state.power_generation = state.power_generation.wrapping_add(1);
517            if !on { state.active_power_lease.take() } else { None }
518        };
519        if on {
520            self.sync_power_control()?;
521        }
522        Ok(())
523    }
524
525    fn sync_power_control(&self) -> Result<(), Errno> {
526        let (generation, token) = {
527            let state = self.state.lock();
528            if !state.power_control_on || state.active_power_lease.is_some() {
529                return Ok(());
530            }
531            if state.provider.is_none() || self.topology.is_none() {
532                return Ok(());
533            }
534            let Some(ref goal_token) = state.power_on_token else {
535                return Ok(());
536            };
537            let Ok(dup_token) = goal_token.duplicate_handle(zx::Rights::SAME_RIGHTS) else {
538                return error!(EIO);
539            };
540            (state.power_generation, dup_token)
541        };
542
543        let lease_result = self.acquire_lease(token, GPU_POWER_LEVEL_ON, "starnix-gpu-power-on");
544        let mut state = self.state.lock();
545        if state.power_generation != generation || !state.power_control_on {
546            drop(state);
547            return lease_result.map(|_| ()).map_err(|()| errno!(EIO));
548        }
549        if let Ok(lease) = lease_result {
550            let prev = state.active_power_lease.replace(lease);
551            drop(state);
552            drop(prev);
553            return Ok(());
554        }
555        drop(state);
556        error!(EIO)
557    }
558
559    fn power_control_is_on(&self) -> bool {
560        self.state.lock().power_control_on
561    }
562
563    fn update_min_freq_vote(&self, voter: &'static str, raw_value: u64) -> Result<(), Errno> {
564        let hz = normalize_gpu_freq_to_hz(raw_value);
565        let snapped = self.config.snap_min_freq_hz(hz);
566
567        {
568            let mut state = self.state.lock();
569            match snapped {
570                Some(freq_hz) => {
571                    state.min_votes.insert(voter, freq_hz);
572                }
573                None => {
574                    state.min_votes.remove(voter);
575                }
576            }
577            state.min_freq_generation = state.min_freq_generation.wrapping_add(1);
578        }
579        self.sync_min_freq()
580    }
581
582    fn sync_min_freq(&self) -> Result<(), Errno> {
583        let (generation, target_hz, provider) = {
584            let mut state = self.state.lock();
585            let effective_min_hz = state.min_votes.values().copied().max();
586            if state.applied_min_freq_hz == effective_min_hz {
587                return Ok(());
588            }
589
590            let Some(target_hz) = effective_min_hz else {
591                let dropped = state.min_freq_lease.take();
592                state.applied_min_freq_hz = None;
593                drop(state);
594                drop(dropped);
595                return Ok(());
596            };
597            let (Some(provider), true) =
598                (state.provider.as_ref().cloned(), self.topology.is_some())
599            else {
600                let dropped = state.min_freq_lease.take();
601                state.applied_min_freq_hz = None;
602                drop(state);
603                drop(dropped);
604                return Ok(());
605            };
606            (state.min_freq_generation, target_hz, provider)
607        };
608
609        let req = fmagma::PowerElementProviderGetClockSpeedLevelRequest {
610            hz: Some(target_hz),
611            allow_max: Some(true),
612            ..Default::default()
613        };
614        // Acquire the new lease outside `self.state.lock()` BEFORE replacing
615        // `state.min_freq_lease` so Power Broker transitions directly without a transient
616        // `GPU_POWER_LEVEL_OFF` dip and without holding the state lock across FIDL calls.
617        let mut reset_provider = false;
618        let mut not_supported = false;
619        let mut sync_result = Ok(());
620        let new_lease = match provider.get_clock_speed_level(&req, fidl_deadline()) {
621            Ok(Ok(resp)) => {
622                if let (Some(token), Some(level)) = (resp.token, resp.level) {
623                    match self.acquire_lease(token, level, "starnix-gpu-min-freq") {
624                        Ok(lease) => Some(lease),
625                        Err(()) => {
626                            sync_result = error!(EIO);
627                            None
628                        }
629                    }
630                } else {
631                    sync_result = error!(EIO);
632                    None
633                }
634            }
635            Ok(Err(status)) => {
636                let zx_status = zx::Status::err_from_raw(status);
637                log_warn!("GetClockSpeedLevel({}) returned status {}", target_hz, zx_status);
638                if zx_status == zx::Status::NOT_SUPPORTED {
639                    not_supported = true;
640                } else {
641                    sync_result = error!(EIO);
642                }
643                None
644            }
645            Err(err) => {
646                log_warn!("GetClockSpeedLevel transport error: {:?}", err);
647                reset_provider = true;
648                sync_result = error!(EIO);
649                None
650            }
651        };
652
653        let _dropped_lease = {
654            let mut state = self.state.lock();
655            if reset_provider && state.provider.as_ref().is_some_and(|p| Arc::ptr_eq(p, &provider))
656            {
657                state.power_on_token = None;
658                state.provider = None;
659            }
660            if state.min_freq_generation != generation {
661                new_lease
662            } else if let Some(lease) = new_lease {
663                state.applied_min_freq_hz = Some(target_hz);
664                state.min_freq_lease.replace(lease)
665            } else if not_supported {
666                state.applied_min_freq_hz = Some(target_hz);
667                state.min_freq_lease.take()
668            } else if state.applied_min_freq_hz != Some(target_hz) {
669                state.applied_min_freq_hz = None;
670                state.min_freq_lease.take()
671            } else {
672                None
673            }
674        };
675        sync_result
676    }
677
678    fn min_voter_freq_hz(&self, voter: &'static str) -> u64 {
679        let state = self.state.lock();
680        if voter == "min_freq" {
681            state.min_votes.values().copied().max().unwrap_or(0)
682        } else {
683            state.min_votes.get(voter).copied().unwrap_or(0)
684        }
685    }
686
687    fn update_max_freq_vote(&self, voter: &'static str, raw_value: u64) -> Result<(), Errno> {
688        let hz = normalize_gpu_freq_to_hz(raw_value);
689        let snapped = self.config.snap_max_freq_hz(hz);
690        {
691            let mut state = self.state.lock();
692            match snapped {
693                Some(freq_hz) => {
694                    state.max_votes.insert(voter, freq_hz);
695                }
696                None => {
697                    state.max_votes.remove(voter);
698                }
699            }
700            state.max_freq_generation = state.max_freq_generation.wrapping_add(1);
701        }
702        self.sync_max_freq()
703    }
704
705    fn sync_max_freq(&self) -> Result<(), Errno> {
706        let (generation, target_hz, provider) = {
707            let mut state = self.state.lock();
708            let effective_max_hz = state.max_votes.values().copied().min();
709            if state.applied_max_freq_hz == effective_max_hz {
710                return Ok(());
711            }
712
713            let Some(target_hz) = effective_max_hz else {
714                let dropped = state.max_freq_limit_handle.take();
715                state.applied_max_freq_hz = None;
716                drop(state);
717                drop(dropped);
718                return Ok(());
719            };
720            let Some(provider) = state.provider.as_ref().cloned() else {
721                let dropped = state.max_freq_limit_handle.take();
722                state.applied_max_freq_hz = None;
723                drop(state);
724                drop(dropped);
725                return Ok(());
726            };
727            (state.max_freq_generation, target_hz, provider)
728        };
729
730        let req = fmagma::PowerElementProviderSetClockLimitRequest {
731            hz: Some(target_hz),
732            ..Default::default()
733        };
734        let mut reset_provider = false;
735        let mut not_supported = false;
736        let mut sync_result = Ok(());
737        let new_handle = match provider.set_clock_limit(&req, fidl_deadline()) {
738            Ok(Ok(resp)) => {
739                if let Some(handle) = resp.handle {
740                    Some(handle)
741                } else {
742                    sync_result = error!(EIO);
743                    None
744                }
745            }
746            Ok(Err(status)) => {
747                let zx_status = zx::Status::err_from_raw(status);
748                log_warn!("SetClockLimit({}) returned status {}", target_hz, zx_status);
749                if zx_status == zx::Status::NOT_SUPPORTED {
750                    not_supported = true;
751                } else {
752                    sync_result = error!(EIO);
753                }
754                None
755            }
756            Err(err) => {
757                log_warn!("SetClockLimit transport error: {:?}", err);
758                reset_provider = true;
759                sync_result = error!(EIO);
760                None
761            }
762        };
763
764        let _dropped_handle = {
765            let mut state = self.state.lock();
766            if reset_provider && state.provider.as_ref().is_some_and(|p| Arc::ptr_eq(p, &provider))
767            {
768                state.power_on_token = None;
769                state.provider = None;
770            }
771            if state.max_freq_generation != generation {
772                new_handle
773            } else if let Some(handle) = new_handle {
774                state.applied_max_freq_hz = Some(target_hz);
775                state.max_freq_limit_handle.replace(handle)
776            } else if not_supported {
777                state.applied_max_freq_hz = Some(target_hz);
778                state.max_freq_limit_handle.take()
779            } else if state.applied_max_freq_hz != Some(target_hz) {
780                state.applied_max_freq_hz = None;
781                state.max_freq_limit_handle.take()
782            } else {
783                None
784            }
785        };
786        sync_result
787    }
788
789    fn max_voter_freq_hz(&self, voter: &'static str) -> u64 {
790        let max_hz = self.config.max_freq_hz();
791        let state = self.state.lock();
792        if voter == "max_freq" {
793            state.max_votes.values().copied().min().unwrap_or(max_hz)
794        } else {
795            state.max_votes.get(voter).copied().unwrap_or(max_hz)
796        }
797    }
798
799    fn cur_freq_hz(&self) -> u64 {
800        let default_hz = self.config.default_freq_hz;
801        let max_hz = self.config.max_freq_hz();
802        let state = self.state.lock();
803        let floor = state.min_votes.values().copied().max().unwrap_or(default_hz);
804        let ceil = state.max_votes.values().copied().min().unwrap_or(max_hz);
805        floor.max(default_hz).min(ceil)
806    }
807
808    fn set_polling_interval_ms(&self, ms: u64) {
809        self.state.lock().polling_interval_ms = ms;
810    }
811
812    fn polling_interval_ms(&self) -> u64 {
813        self.state.lock().polling_interval_ms
814    }
815}
816
817struct GpuPowerControlFile {
818    domain: Arc<GpuPowerDomain>,
819}
820
821impl GpuPowerControlFile {
822    fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
823        BytesFile::new_node(Self { domain })
824    }
825}
826
827impl BytesFileOps for GpuPowerControlFile {
828    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
829        let s: &[u8] = if self.domain.power_control_is_on() { b"on\n" } else { b"auto\n" };
830        Ok(s.into())
831    }
832
833    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
834        let s = std::str::from_utf8(&data).map_err(|_| errno!(EINVAL))?.trim();
835        match s {
836            "on" => self.domain.set_power_control(true),
837            "auto" => self.domain.set_power_control(false),
838            _ => error!(EINVAL),
839        }
840    }
841}
842
843struct GpuRuntimeStatusFile {
844    domain: Arc<GpuPowerDomain>,
845}
846
847impl GpuRuntimeStatusFile {
848    fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
849        BytesFile::new_node(Self { domain })
850    }
851}
852
853impl BytesFileOps for GpuRuntimeStatusFile {
854    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
855        let s: &[u8] = if self.domain.power_control_is_on() { b"active\n" } else { b"suspended\n" };
856        Ok(s.into())
857    }
858}
859
860struct GpuFreqVoteFile<const IS_MAX: bool> {
861    domain: Arc<GpuPowerDomain>,
862    voter: &'static str,
863}
864
865impl<const IS_MAX: bool> GpuFreqVoteFile<IS_MAX> {
866    fn new_node(domain: Arc<GpuPowerDomain>, voter: &'static str) -> impl FsNodeOps {
867        BytesFile::new_node(Self { domain, voter })
868    }
869}
870
871impl<const IS_MAX: bool> BytesFileOps for GpuFreqVoteFile<IS_MAX> {
872    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
873        let freq_hz = if IS_MAX {
874            self.domain.max_voter_freq_hz(self.voter)
875        } else {
876            self.domain.min_voter_freq_hz(self.voter)
877        };
878        Ok(format!("{freq_hz}\n").into_bytes().into())
879    }
880
881    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
882        let val = parse_unsigned_file::<u64>(&data)?;
883        if IS_MAX {
884            self.domain.update_max_freq_vote(self.voter, val)
885        } else {
886            self.domain.update_min_freq_vote(self.voter, val)
887        }
888    }
889}
890
891struct GpuCurFreqFile {
892    domain: Arc<GpuPowerDomain>,
893}
894
895impl GpuCurFreqFile {
896    fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
897        BytesFile::new_node(Self { domain })
898    }
899}
900
901impl BytesFileOps for GpuCurFreqFile {
902    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
903        Ok(format!("{}\n", self.domain.cur_freq_hz()).into_bytes().into())
904    }
905}
906
907struct GpuPollingIntervalFile {
908    domain: Arc<GpuPowerDomain>,
909}
910
911impl GpuPollingIntervalFile {
912    fn new_node(domain: Arc<GpuPowerDomain>) -> impl FsNodeOps {
913        BytesFile::new_node(Self { domain })
914    }
915}
916
917impl BytesFileOps for GpuPollingIntervalFile {
918    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
919        Ok(format!("{}\n", self.domain.polling_interval_ms()).into_bytes().into())
920    }
921
922    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
923        let ms = parse_unsigned_file::<u64>(&data)?;
924        self.domain.set_polling_interval_ms(ms);
925        Ok(())
926    }
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use fidl::endpoints::create_endpoints;
933    use fidl_fuchsia_io as fio;
934    use fuchsia_component::server::ServiceFs;
935    use futures::StreamExt;
936    use starnix_core::testing::spawn_kernel_and_run;
937    use starnix_sync::Mutex;
938    use std::sync::atomic::{AtomicU64, Ordering};
939
940    #[::fuchsia::test]
941    async fn sysfs_registers_gpu_devfreq_and_genpd_nodes() {
942        spawn_kernel_and_run(async |current_task| {
943            let kernel = current_task.kernel();
944            register_gpu_devices(kernel, GpuDeviceConfig::default());
945            let root = &kernel.device_registry.objects.root;
946
947            assert!(root.lookup("devices/genpd:0:34f00000.gpu0/power/control".into()).is_some());
948            assert!(
949                root.lookup("devices/genpd:0:34f00000.gpu0/power/runtime_status".into()).is_some()
950            );
951            assert!(root.lookup("class/devfreq/34f00000.gpu0".into()).is_some());
952            assert!(root.lookup("bus/platform/devices/34f00000.gpu0".into()).is_some());
953            assert!(root.lookup("devices/platform/34f00000.gpu0/power/control".into()).is_some());
954            assert!(
955                root.lookup(
956                    "devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/vote_manager/powerhint_min_freq"
957                        .into()
958                )
959                .is_some()
960            );
961            assert!(
962                root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/min_freq".into())
963                    .is_some()
964            );
965            assert!(
966                root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/max_freq".into())
967                    .is_some()
968            );
969            assert!(
970                root.lookup("devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/cur_freq".into())
971                    .is_some()
972            );
973            assert!(
974                root.lookup(
975                    "devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/available_frequencies"
976                        .into()
977                )
978                .is_some()
979            );
980            assert!(
981                root.lookup(
982                    "devices/platform/34f00000.gpu0/devfreq/34f00000.gpu0/polling_interval".into()
983                )
984                .is_some()
985            );
986        })
987        .await;
988    }
989
990    struct MockPowerBackend {
991        power_token_koid: zx::Koid,
992        clock_token_koid: zx::Koid,
993        power_token: zx::Event,
994        clock_token: zx::Event,
995        leased_power_koid: Arc<AtomicU64>,
996        leased_clock_koid: Arc<AtomicU64>,
997        leased_clock_level: Arc<AtomicU64>,
998        capped_clock_hz: Arc<AtomicU64>,
999        clock_speed_calls: Arc<AtomicU64>,
1000        active_broker_tokens: Arc<Mutex<Vec<zx::EventPair>>>,
1001        active_limit_tokens: Arc<Mutex<Vec<zx::EventPair>>>,
1002    }
1003
1004    impl MockPowerBackend {
1005        fn new() -> Self {
1006            let power_token = zx::Event::create();
1007            let power_token_koid = power_token.koid().expect("koid");
1008            let clock_token = zx::Event::create();
1009            let clock_token_koid = clock_token.koid().expect("koid");
1010            Self {
1011                power_token_koid,
1012                clock_token_koid,
1013                power_token,
1014                clock_token,
1015                leased_power_koid: Arc::new(AtomicU64::new(0)),
1016                leased_clock_koid: Arc::new(AtomicU64::new(0)),
1017                leased_clock_level: Arc::new(AtomicU64::new(0)),
1018                capped_clock_hz: Arc::new(AtomicU64::new(0)),
1019                clock_speed_calls: Arc::new(AtomicU64::new(0)),
1020                active_broker_tokens: Arc::new(Mutex::new(Vec::new())),
1021                active_limit_tokens: Arc::new(Mutex::new(Vec::new())),
1022            }
1023        }
1024
1025        fn spawn_provider(
1026            &self,
1027            scope: &fasync::Scope,
1028            mut stream: fmagma::PowerElementProviderRequestStream,
1029            reject_576mhz_not_supported: bool,
1030        ) {
1031            let power_token =
1032                self.power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
1033            let clock_token =
1034                self.clock_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
1035            let capped_clock_recorder = self.capped_clock_hz.clone();
1036            let clock_speed_calls = self.clock_speed_calls.clone();
1037            let limit_tokens_holder = self.active_limit_tokens.clone();
1038            scope.spawn(async move {
1039                while let Some(Ok(req)) = stream.next().await {
1040                    match req {
1041                        fmagma::PowerElementProviderRequest::GetPowerGoals { responder } => {
1042                            let dup =
1043                                power_token.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("dup");
1044                            let _ = responder.send(vec![fmagma::PowerGoal {
1045                                type_: Some(fmagma::PowerGoalType::OnReadyForWork),
1046                                token: Some(dup),
1047                                ..Default::default()
1048                            }]);
1049                        }
1050                        fmagma::PowerElementProviderRequest::GetClockSpeedLevel {
1051                            payload,
1052                            responder,
1053                        } => {
1054                            clock_speed_calls.fetch_add(1, Ordering::SeqCst);
1055                            if reject_576mhz_not_supported && payload.hz == Some(576_000_000) {
1056                                let _ = responder.send(Err(zx::Status::NOT_SUPPORTED.into_raw()));
1057                            } else {
1058                                let dup = clock_token
1059                                    .duplicate_handle(zx::Rights::SAME_RIGHTS)
1060                                    .expect("dup");
1061                                let level = if payload.hz == Some(576_000_000) { 6u8 } else { 3u8 };
1062                                let _ = responder.send(Ok(
1063                                    fmagma::PowerElementProviderGetClockSpeedLevelResponse {
1064                                        token: Some(dup),
1065                                        level: Some(level),
1066                                        actual_hz: payload.hz,
1067                                        ..Default::default()
1068                                    },
1069                                ));
1070                            }
1071                        }
1072                        fmagma::PowerElementProviderRequest::SetClockLimit {
1073                            payload,
1074                            responder,
1075                        } => {
1076                            if reject_576mhz_not_supported && payload.hz == Some(576_000_000) {
1077                                let _ = responder.send(Err(zx::Status::NOT_SUPPORTED.into_raw()));
1078                            } else if reject_576mhz_not_supported && payload.hz == Some(396_000_000)
1079                            {
1080                                let _ = responder.send(Err(zx::Status::INTERNAL.into_raw()));
1081                            } else {
1082                                let (client_ep, server_ep) = zx::EventPair::create();
1083                                capped_clock_recorder
1084                                    .store(payload.hz.unwrap_or(0), Ordering::SeqCst);
1085                                limit_tokens_holder.lock().push(server_ep);
1086                                let _ = responder.send(Ok(
1087                                    fmagma::PowerElementProviderSetClockLimitResponse {
1088                                        handle: Some(client_ep),
1089                                        actual_hz: payload.hz,
1090                                        ..Default::default()
1091                                    },
1092                                ));
1093                            }
1094                        }
1095                        _ => {}
1096                    }
1097                }
1098            });
1099        }
1100
1101        fn spawn_topology(
1102            &self,
1103            scope: &fasync::Scope,
1104            topology_server: fidl::endpoints::ServerEnd<fpbroker::TopologyMarker>,
1105            reject_leases: bool,
1106        ) {
1107            let power_token_raw = self.power_token_koid.raw_koid();
1108            let power_koid_recorder = self.leased_power_koid.clone();
1109            let clock_koid_recorder = self.leased_clock_koid.clone();
1110            let clock_level_recorder = self.leased_clock_level.clone();
1111            let tokens_holder = self.active_broker_tokens.clone();
1112            scope.spawn(async move {
1113                let mut stream = topology_server.into_stream();
1114                while let Some(Ok(req)) = stream.next().await {
1115                    if let fpbroker::TopologyRequest::Lease { payload, responder } = req {
1116                        assert_eq!(payload.should_return_pending_lease, Some(true));
1117                        if reject_leases {
1118                            let _ = responder.send(Err(fpbroker::LeaseError::Internal));
1119                            continue;
1120                        }
1121                        if let Some(deps) = payload.dependencies {
1122                            for dep in deps {
1123                                let koid = dep
1124                                    .requires_token
1125                                    .as_ref()
1126                                    .and_then(|t| t.koid().ok())
1127                                    .map(|k| k.raw_koid())
1128                                    .unwrap_or(0);
1129                                if koid == power_token_raw {
1130                                    power_koid_recorder.store(koid, Ordering::SeqCst);
1131                                } else {
1132                                    clock_koid_recorder.store(koid, Ordering::SeqCst);
1133                                    clock_level_recorder.store(
1134                                        dep.requires_level.unwrap_or(GPU_POWER_LEVEL_OFF) as u64,
1135                                        Ordering::SeqCst,
1136                                    );
1137                                }
1138                            }
1139                        }
1140                        if let Some(lt) = payload.lease_token {
1141                            tokens_holder.lock().push(lt);
1142                        }
1143                        let _ = responder.send(Ok(()));
1144                    }
1145                }
1146            });
1147        }
1148    }
1149
1150    #[::fuchsia::test]
1151    async fn gpu_power_domain_leases_power_broker_on_hints() {
1152        let (provider_client, provider_server) =
1153            create_endpoints::<fmagma::PowerElementProviderMarker>();
1154        let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
1155
1156        let backend = MockPowerBackend::new();
1157        let scope = fasync::Scope::new();
1158        backend.spawn_provider(&scope, provider_server.into_stream(), false);
1159        backend.spawn_topology(&scope, topology_server, false);
1160
1161        let domain = fasync::unblock(move || {
1162            let domain = GpuPowerDomain::with_proxies(
1163                fmagma::PowerElementProviderSynchronousProxy::new(provider_client.into_channel()),
1164                fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
1165            );
1166
1167            // 1. Acquire `GPUPowerOn` ("on") and `GPUMinFreq` ("576000" kHz -> 576 MHz).
1168            domain.set_power_control(true).expect("set power control on");
1169            assert!(domain.power_control_is_on());
1170            domain.update_min_freq_vote("powerhint_min_freq", 576_000).expect("vote 576000 kHz");
1171            assert_eq!(domain.min_voter_freq_hz("min_freq"), 576_000_000);
1172            assert_eq!(domain.cur_freq_hz(), 576_000_000);
1173
1174            // 2. Release votes ("auto" and "0").
1175            domain.update_min_freq_vote("powerhint_min_freq", 0).expect("clear min_freq");
1176            assert_eq!(domain.min_voter_freq_hz("min_freq"), 0);
1177            assert_eq!(domain.cur_freq_hz(), 396_000_000);
1178            domain.update_max_freq_vote("powerhint_max_freq", 300_000).expect("cap max_freq");
1179            assert_eq!(domain.max_voter_freq_hz("max_freq"), 300_000_000);
1180            assert_eq!(domain.cur_freq_hz(), 300_000_000);
1181            domain.update_max_freq_vote("powerhint_max_freq", 0).expect("clear max_freq with 0");
1182            assert_eq!(domain.max_voter_freq_hz("max_freq"), 1_094_000_000);
1183            assert_eq!(domain.cur_freq_hz(), 396_000_000);
1184            domain.set_power_control(false).expect("set power control auto");
1185            assert!(!domain.power_control_is_on());
1186            domain
1187        })
1188        .await;
1189
1190        assert_eq!(
1191            backend.leased_power_koid.load(Ordering::SeqCst),
1192            backend.power_token_koid.raw_koid()
1193        );
1194        assert_eq!(
1195            backend.leased_clock_koid.load(Ordering::SeqCst),
1196            backend.clock_token_koid.raw_koid()
1197        );
1198        assert_eq!(backend.leased_clock_level.load(Ordering::SeqCst), 6);
1199        assert_eq!(backend.capped_clock_hz.load(Ordering::SeqCst), 300_000_000);
1200
1201        // Verify both server-side lease tokens received `EVENTPAIR_PEER_CLOSED` on release!
1202        let broker_tokens = backend.active_broker_tokens.lock();
1203        assert_eq!(broker_tokens.len(), 2);
1204        for bt in broker_tokens.iter() {
1205            let signals = bt
1206                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST);
1207            assert!(signals.is_ok());
1208        }
1209
1210        // Verify the SetClockLimit handle also received `EVENTPAIR_PEER_CLOSED` when cleared!
1211        let limit_tokens = backend.active_limit_tokens.lock();
1212        assert_eq!(limit_tokens.len(), 1);
1213        for lt in limit_tokens.iter() {
1214            let signals = lt
1215                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST);
1216            assert!(signals.is_ok());
1217        }
1218        drop(domain);
1219    }
1220
1221    #[::fuchsia::test]
1222    async fn watch_power_service_reconciles_active_votes_on_provider_arrival() {
1223        let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
1224        let backend = Arc::new(MockPowerBackend::new());
1225        let scope = fasync::Scope::new();
1226        backend.spawn_topology(&scope, topology_server, false);
1227
1228        let (dir_client, dir_server) = create_endpoints::<fio::DirectoryMarker>();
1229        let mut fs = ServiceFs::new();
1230        fs.add_fidl_service_instance("default", |req: fmagma::PowerServiceRequest| req);
1231        fs.serve_connection(dir_server).expect("serve PowerService directory");
1232        let backend_for_fs = Arc::clone(&backend);
1233        scope.spawn(async move {
1234            let inner_scope = fasync::Scope::new();
1235            while let Some(fmagma::PowerServiceRequest::PowerElementProvider(stream)) =
1236                fs.next().await
1237            {
1238                backend_for_fs.spawn_provider(&inner_scope, stream, false);
1239            }
1240        });
1241
1242        let domain = Arc::new(GpuPowerDomain::with_topology(
1243            fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
1244        ));
1245
1246        // Record power and min-frequency votes BEFORE any `PowerElementProvider` is connected.
1247        domain.set_power_control(true).expect("power control before provider");
1248        domain.update_min_freq_vote("powerhint_min_freq", 576_000).expect("vote before provider");
1249        assert_eq!(backend.leased_power_koid.load(Ordering::SeqCst), 0);
1250        assert_eq!(backend.leased_clock_koid.load(Ordering::SeqCst), 0);
1251
1252        // Now start `watch_power_service` against the ServiceFs directory and verify both active
1253        // votes are automatically reconciled as soon as the instance is discovered.
1254        let service =
1255            fclient::Service::open_from_dir(dir_client, fmagma::PowerServiceMarker).expect("open");
1256        let watcher_domain = Arc::clone(&domain);
1257        scope.spawn(async move {
1258            let _ = watch_power_service(watcher_domain, service).await;
1259        });
1260
1261        while backend.leased_power_koid.load(Ordering::SeqCst) == 0
1262            || backend.leased_clock_koid.load(Ordering::SeqCst) == 0
1263        {
1264            fasync::Timer::new(zx::MonotonicDuration::from_millis(10)).await;
1265        }
1266
1267        assert_eq!(
1268            backend.leased_power_koid.load(Ordering::SeqCst),
1269            backend.power_token_koid.raw_koid()
1270        );
1271        assert_eq!(
1272            backend.leased_clock_koid.load(Ordering::SeqCst),
1273            backend.clock_token_koid.raw_koid()
1274        );
1275        assert_eq!(backend.leased_clock_level.load(Ordering::SeqCst), 6);
1276    }
1277
1278    #[::fuchsia::test]
1279    async fn gpu_power_domain_returns_eio_on_lease_error_and_allows_not_supported() {
1280        let (provider_client, provider_server) =
1281            create_endpoints::<fmagma::PowerElementProviderMarker>();
1282        let (topology_client, topology_server) = create_endpoints::<fpbroker::TopologyMarker>();
1283
1284        let backend = MockPowerBackend::new();
1285        let scope = fasync::Scope::new();
1286        backend.spawn_provider(&scope, provider_server.into_stream(), true);
1287        backend.spawn_topology(&scope, topology_server, true);
1288
1289        fasync::unblock(move || {
1290            let domain = GpuPowerDomain::with_proxies(
1291                fmagma::PowerElementProviderSynchronousProxy::new(provider_client.into_channel()),
1292                fpbroker::TopologySynchronousProxy::new(topology_client.into_channel()),
1293            );
1294
1295            // 1. Topology::Lease rejection on `GPUPowerOn` returns EIO.
1296            assert_eq!(domain.set_power_control(true), error!(EIO));
1297
1298            // 2. `ZX_ERR_NOT_SUPPORTED` from `GetClockSpeedLevel` is gracefully ignored (Ok(()))
1299            // and cached in `applied_min_freq_hz` so redundant votes at the same target Hz do not
1300            // make additional FIDL calls.
1301            assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 576_000), Ok(()));
1302            assert_eq!(domain.update_min_freq_vote("soft_min_freq", 576_000), Ok(()));
1303            assert_eq!(domain.update_min_freq_vote("soft_min_freq", 0), Ok(()));
1304
1305            // 3. Topology::Lease rejection on `GetClockSpeedLevel` returns EIO.
1306            assert_eq!(domain.update_min_freq_vote("powerhint_min_freq", 396_000), error!(EIO));
1307
1308            // 4. `ZX_ERR_NOT_SUPPORTED` from `SetClockLimit` is gracefully ignored (Ok(())), while
1309            // non-`NOT_SUPPORTED` errors propagate EIO.
1310            assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 576_000), Ok(()));
1311            assert_eq!(domain.update_max_freq_vote("powerhint_max_freq", 396_000), error!(EIO));
1312        })
1313        .await;
1314        // 576_000_000 was queried once (cached across the two redundant votes) and 396_000_000
1315        // was queried once.
1316        assert_eq!(backend.clock_speed_calls.load(Ordering::SeqCst), 2);
1317    }
1318}