Skip to main content

keyhog_scanner/compiled_scanner/
types.rs

1//! Public scanner lifecycle and backend-readiness types.
2
3use crate::hw_probe::ScanBackend;
4use std::sync::{Arc, OnceLock};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum GpuInitPolicy {
8    /// Honor the resolved GPU runtime policy.
9    FromRuntimePolicy,
10    /// Census GPU peers regardless of the disabled-GPU policy. The selected
11    /// execution backend is still materialized lazily.
12    ForceEnabled,
13    /// Skip CUDA/WGPU census and acquisition. Used when the selected CLI path
14    /// cannot route to GPU, avoiding startup and RSS overhead without changing
15    /// scan results.
16    ForceDisabled,
17}
18
19pub(crate) struct GpuBackendPeers {
20    cuda: OnceLock<Result<AcquiredGpuPeer, String>>,
21    wgpu: OnceLock<Result<AcquiredGpuPeer, String>>,
22    pub(crate) cuda_available: bool,
23    pub(crate) wgpu_available: bool,
24    pub(crate) cuda_device_identity: Option<String>,
25    pub(crate) cuda_runtime_identity: Option<String>,
26    pub(crate) wgpu_device_identity: Option<String>,
27    pub(crate) wgpu_runtime_identity: Option<String>,
28    pub(crate) wgpu_is_software: bool,
29}
30
31pub(crate) struct AcquiredGpuPeer {
32    pub(crate) backend: Arc<dyn vyre::VyreBackend>,
33    pub(crate) device_identity: Option<String>,
34    pub(crate) is_software: bool,
35}
36
37impl Default for GpuBackendPeers {
38    fn default() -> Self {
39        Self {
40            cuda: OnceLock::new(),
41            wgpu: OnceLock::new(),
42            cuda_available: false,
43            wgpu_available: false,
44            cuda_device_identity: None,
45            cuda_runtime_identity: None,
46            wgpu_device_identity: None,
47            wgpu_runtime_identity: None,
48            wgpu_is_software: false,
49        }
50    }
51}
52
53impl GpuBackendPeers {
54    pub(crate) fn get(&self, backend: ScanBackend) -> Option<&Arc<dyn vyre::VyreBackend>> {
55        match backend {
56            ScanBackend::GpuCuda if self.cuda_available => self
57                .cuda
58                .get_or_init(acquire_cuda_peer)
59                .as_ref()
60                // LAW10: diagnostic accessor; acquisition failures remain stored and exposed by runtime diagnostics while this accessor asks only whether a usable peer exists.
61                .ok()
62                .map(|peer| &peer.backend),
63            ScanBackend::GpuWgpu if self.wgpu_available => self
64                .wgpu
65                .get_or_init(acquire_wgpu_peer)
66                .as_ref()
67                // LAW10: diagnostic accessor; acquisition failures remain stored and exposed by runtime diagnostics while this accessor asks only whether a usable peer exists.
68                .ok()
69                .map(|peer| &peer.backend),
70            _ => None,
71        }
72    }
73
74    pub(crate) fn initialized(&self, backend: ScanBackend) -> Option<&AcquiredGpuPeer> {
75        match backend {
76            // LAW10: diagnostic accessor; acquisition errors remain stored while this accessor returns only successfully acquired peers.
77            ScanBackend::GpuCuda => self.cuda.get().and_then(|result| result.as_ref().ok()),
78            // LAW10: diagnostic accessor; WGPU acquisition errors remain stored while this accessor returns only a successfully acquired peer.
79            ScanBackend::GpuWgpu => self.wgpu.get().and_then(|result| result.as_ref().ok()),
80            _ => None,
81        }
82    }
83
84    pub(crate) fn initialization_error(&self, backend: ScanBackend) -> Option<&str> {
85        match backend {
86            ScanBackend::GpuCuda => self.cuda.get(),
87            ScanBackend::GpuWgpu => self.wgpu.get(),
88            _ => None,
89        }
90        .and_then(|result| result.as_ref().err().map(String::as_str))
91    }
92
93    pub(crate) fn availability(&self) -> GpuBackendAvailability {
94        GpuBackendAvailability {
95            cuda: self.cuda_available,
96            wgpu: self.wgpu_available,
97        }
98    }
99}
100
101#[cfg(all(feature = "gpu", target_os = "linux"))]
102fn acquire_cuda_peer() -> Result<AcquiredGpuPeer, String> {
103    let backend = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
104        let cuda = vyre_driver_cuda::backend::CudaBackend::acquire()?;
105        let boxed: Box<dyn vyre::VyreBackend> =
106            Box::new(vyre_driver_cuda::CudaBackendRegistration::new(cuda));
107        Ok::<Arc<dyn vyre::VyreBackend>, String>(Arc::from(boxed))
108    }))
109    .map_err(|panic| {
110        format!(
111            "CUDA backend acquisition panicked: {}. Fix: repair the CUDA driver/runtime or select another calibrated backend",
112            crate::error::panic_payload_detail(panic)
113        )
114    })??;
115    tracing::info!(target: "keyhog::routing", "selected CUDA peer backend acquired");
116    Ok(AcquiredGpuPeer {
117        backend,
118        device_identity: None,
119        is_software: false,
120    })
121}
122
123#[cfg(not(all(feature = "gpu", target_os = "linux")))]
124fn acquire_cuda_peer() -> Result<AcquiredGpuPeer, String> {
125    Err("CUDA peer is not compiled for this platform".to_string())
126}
127
128#[cfg(feature = "gpu")]
129fn acquire_wgpu_peer() -> Result<AcquiredGpuPeer, String> {
130    let backend = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
131        vyre_driver_wgpu::WgpuBackend::shared,
132    ))
133    .map_err(|panic| {
134        format!(
135            "WGPU backend acquisition panicked: {}. Fix: repair the graphics driver/runtime or select another calibrated backend",
136            crate::error::panic_payload_detail(panic)
137        )
138    })?
139    .map_err(|error| error.to_string())?;
140    let info = backend.adapter_info();
141    let device_identity =
142        crate::gpu::gpu_adapter_device_identity(info, backend.device_limits().max_buffer_size);
143    let is_software = crate::gpu::is_software_adapter(info);
144    tracing::info!(
145        target: "keyhog::routing",
146        device_identity,
147        "selected WGPU peer backend acquired"
148    );
149    let backend: Arc<dyn vyre::VyreBackend> = backend;
150    Ok(AcquiredGpuPeer {
151        backend,
152        device_identity: Some(device_identity),
153        is_software,
154    })
155}
156
157#[cfg(all(feature = "gpu", target_os = "linux"))]
158pub(crate) fn probe_cuda_peer() -> Result<vyre_driver_cuda::device::CudaDeviceCaps, String> {
159    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
160        vyre_driver_cuda::device::CudaDeviceCaps::probe(0)
161    }))
162    .map_err(|panic| {
163        format!(
164            "CUDA device probe panicked: {}. Fix: repair the CUDA driver/runtime before enabling this backend",
165            crate::error::panic_payload_detail(panic)
166        )
167    })?
168    .map_err(|error| error.to_string())
169}
170
171#[cfg(not(feature = "gpu"))]
172fn acquire_wgpu_peer() -> Result<AcquiredGpuPeer, String> {
173    Err("WGPU peer is not compiled in this build".to_string())
174}
175
176#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
177pub struct GpuBackendAvailability {
178    pub cuda: bool,
179    pub wgpu: bool,
180}
181
182impl GpuBackendAvailability {
183    #[must_use]
184    pub const fn any(self) -> bool {
185        self.cuda || self.wgpu
186    }
187}
188
189#[derive(Clone, Debug, Eq, PartialEq)]
190pub(crate) struct GpuBackendAcquisitionFailure {
191    pub backend: &'static str,
192    pub diagnostic: String,
193}
194
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct GpuBackendCandidateStatus {
197    pub backend: ScanBackend,
198    /// Whether the lightweight host census found a hardware peer with enough
199    /// identity to participate in autoroute.
200    pub available: bool,
201    /// Whether this process has materialized the execution backend.
202    pub acquired: bool,
203    pub driver_id: Option<&'static str>,
204    pub driver_version: Option<&'static str>,
205    pub device_identity: Option<String>,
206    pub runtime_identity: Option<String>,
207    pub is_software: bool,
208    pub acquisition_error: Option<String>,
209}
210
211impl GpuBackendCandidateStatus {
212    #[must_use]
213    pub fn has_complete_identity(&self) -> bool {
214        self.driver_id.is_some_and(|value| !value.trim().is_empty())
215            && self
216                .driver_version
217                .is_some_and(|value| !value.trim().is_empty())
218            && self
219                .device_identity
220                .as_deref()
221                .is_some_and(|value| !value.trim().is_empty())
222            && self
223                .runtime_identity
224                .as_deref()
225                .is_some_and(|value| !value.trim().is_empty())
226    }
227
228    /// Whether the lightweight census found hardware with complete identity.
229    /// This makes the peer eligible for materialization, but does not prove
230    /// that device acquisition has succeeded.
231    #[must_use]
232    pub fn is_eligible(&self) -> bool {
233        self.available && !self.is_software && self.has_complete_identity()
234    }
235
236    /// Whether this exact peer has materialized and retains complete identity.
237    #[must_use]
238    pub fn is_acquired_eligible(&self) -> bool {
239        self.acquired && self.is_eligible()
240    }
241}
242
243#[derive(Clone, Copy, Debug, Eq, PartialEq)]
244pub struct CompiledScannerRuntime {
245    pub detector_count: usize,
246    pub pattern_count: usize,
247    /// Versioned 64-bit projection of the canonical 256-bit scan-execution
248    /// hash. Autoroute also persists the complete hash as its rules identity.
249    pub detector_digest: u64,
250    /// Backend used by the no-backend library APIs. CLI calibrated routing is a
251    /// separate persisted per-workload decision and is never inferred here.
252    pub preferred_backend: &'static str,
253    pub gpu_backends: GpuBackendAvailability,
254    pub gpu_degrade_count: u64,
255}