1use vyre_driver::error::{Error, Result};
28
29use crate::staging_reserve::reserve_backend_vec;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub(crate) struct AdapterIdentity {
34 name: String,
35 vendor: u32,
36 device: u32,
37 device_type: wgpu::DeviceType,
38 driver: String,
39 driver_info: String,
40 backend: wgpu::Backend,
41}
42
43impl AdapterIdentity {
44 pub(crate) fn from_info(info: &wgpu::AdapterInfo) -> Self {
45 Self {
46 name: info.name.clone(),
47 vendor: info.vendor,
48 device: info.device,
49 device_type: info.device_type,
50 driver: info.driver.clone(),
51 driver_info: info.driver_info.clone(),
52 backend: info.backend,
53 }
54 }
55
56 fn matches(&self, info: &wgpu::AdapterInfo) -> bool {
57 self.name == info.name
58 && self.vendor == info.vendor
59 && self.device == info.device
60 && self.device_type == info.device_type
61 && self.driver == info.driver
62 && self.driver_info == info.driver_info
63 && self.backend == info.backend
64 }
65}
66
67#[derive(Debug, Default, Clone)]
69pub struct AdapterCriteria {
70 pub device_type: Option<wgpu::DeviceType>,
72 pub vendor: Option<u32>,
74 pub name_contains: Option<String>,
77 pub power: Option<wgpu::PowerPreference>,
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct AdapterProbeReport {
84 pub probed: Vec<String>,
86 pub missing: Vec<String>,
88}
89
90impl AdapterCriteria {
91 #[must_use]
93 pub fn high_performance() -> Self {
94 Self {
95 device_type: Some(wgpu::DeviceType::DiscreteGpu),
96 power: Some(wgpu::PowerPreference::HighPerformance),
97 ..Self::default()
98 }
99 }
100
101 #[must_use]
104 pub fn low_power() -> Self {
105 Self {
106 device_type: Some(wgpu::DeviceType::IntegratedGpu),
107 power: Some(wgpu::PowerPreference::LowPower),
108 ..Self::default()
109 }
110 }
111}
112
113#[must_use]
115pub fn enumerate_adapters() -> Vec<wgpu::AdapterInfo> {
116 match try_enumerate_adapters() {
117 Ok(adapters) => adapters,
118 Err(error) => {
119 tracing::error!(
122 %error,
123 "adapter enumeration probe failed; reporting zero adapters is a probe error, not an absence of GPUs"
124 );
125 Vec::new()
126 }
127 }
128}
129
130pub(crate) fn try_enumerate_adapters() -> Result<Vec<wgpu::AdapterInfo>> {
136 let instance = wgpu::Instance::default();
137 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
138 let mut infos = Vec::new();
139 reserve_probe_vec(&mut infos, adapters.len(), "adapter enumeration metadata")?;
140 infos.extend(adapters.iter().map(wgpu::Adapter::get_info));
141 Ok(infos)
142}
143
144#[must_use]
146pub fn has_real_gpu_adapter() -> bool {
147 let instance = wgpu::Instance::default();
148 instance
149 .enumerate_adapters(wgpu::Backends::all())
150 .iter()
151 .any(|adapter| crate::capabilities::is_real_gpu(&adapter.get_info()))
152}
153
154pub fn adapter_for_info(expected: &wgpu::AdapterInfo) -> Result<wgpu::Adapter> {
164 let instance = wgpu::Instance::default();
165 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
166 let mut probed = Vec::new();
167 reserve_probe_vec(&mut probed, adapters.len(), "adapter recovery probe report")?;
168 for adapter in adapters {
169 let candidate = adapter.get_info();
170 if adapter_info_matches(&candidate, expected) {
171 return Ok(adapter);
172 }
173 probed.push(format!(
174 "{} ({:?}, backend={:?}, vendor={:08x}, device={:08x})",
175 candidate.name,
176 candidate.device_type,
177 candidate.backend,
178 candidate.vendor,
179 candidate.device
180 ));
181 }
182
183 Err(Error::Gpu {
184 message: format!(
185 "selected adapter `{}` ({:?}, backend={:?}, vendor={:08x}, device={:08x}) is no longer enumerable. Probed adapters: [{}]. Fix: repair GPU visibility or reacquire the WGPU backend.",
186 expected.name,
187 expected.device_type,
188 expected.backend,
189 expected.vendor,
190 expected.device,
191 probed.join(", ")
192 ),
193 })
194}
195
196fn adapter_info_matches(candidate: &wgpu::AdapterInfo, expected: &wgpu::AdapterInfo) -> bool {
197 candidate.name == expected.name
198 && candidate.vendor == expected.vendor
199 && candidate.device == expected.device
200 && candidate.device_type == expected.device_type
201 && candidate.driver == expected.driver
202 && candidate.driver_info == expected.driver_info
203 && candidate.backend == expected.backend
204}
205
206#[must_use]
208pub fn adapter_probe_report() -> AdapterProbeReport {
209 let instance = wgpu::Instance::default();
210 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
211 let mut report = AdapterProbeReport {
212 probed: Vec::new(),
213 missing: Vec::new(),
214 };
215
216 for adapter in adapters {
217 let info = adapter.get_info();
218 report.probed.push(format!(
219 "{} ({:?}, backend={:?})",
220 info.name, info.device_type, info.backend
221 ));
222 if matches!(
223 info.device_type,
224 wgpu::DeviceType::Cpu | wgpu::DeviceType::Other
225 ) {
226 continue;
227 }
228 if !adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
229 report.missing.push("TIMESTAMP_QUERY".to_string());
230 }
231 if !adapter
232 .features()
233 .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS)
234 {
235 report
236 .missing
237 .push("TIMESTAMP_QUERY_INSIDE_ENCODERS".to_string());
238 }
239 let adapter_limits = adapter.limits();
240 if let Err(error) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
241 label: Some("vyre probe"),
242 required_features: wgpu::Features::empty(),
243 required_limits: wgpu::Limits {
244 max_storage_buffers_per_shader_stage:
245 adapter_limits.max_storage_buffers_per_shader_stage,
246 ..wgpu::Limits::default()
247 },
248 memory_hints: wgpu::MemoryHints::default(),
249 trace: wgpu::Trace::Off,
250 })) {
251 report
252 .missing
253 .push(format!("device request failed on {}: {error}", info.name));
254 }
255 }
256
257 report
258}
259
260pub fn select_adapter(criteria: &AdapterCriteria) -> Result<(usize, wgpu::AdapterInfo)> {
267 let instance = wgpu::Instance::default();
268 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
269 for (idx, adapter) in adapters.iter().enumerate() {
270 let info = adapter.get_info();
271 if adapter_is_selectable(&info, criteria) {
272 return Ok((idx, info));
273 }
274 }
275 Err(Error::Gpu {
276 message: format!(
277 "no real GPU adapter matches criteria {criteria:?}. Fix: loosen the criteria or install drivers exposing the requested GPU class."
278 ),
279 })
280}
281
282fn adapter_is_selectable(info: &wgpu::AdapterInfo, criteria: &AdapterCriteria) -> bool {
283 crate::capabilities::is_real_gpu(info) && adapter_matches(info, criteria)
284}
285
286fn adapter_matches(info: &wgpu::AdapterInfo, criteria: &AdapterCriteria) -> bool {
287 if let Some(ty) = criteria.device_type {
288 if info.device_type != ty {
289 return false;
290 }
291 }
292 if let Some(vendor) = criteria.vendor {
293 if info.vendor != vendor {
294 return false;
295 }
296 }
297 if let Some(needle) = &criteria.name_contains {
298 if !adapter_name_contains(&info.name, needle) {
299 return false;
300 }
301 }
302 true
303}
304
305fn adapter_name_contains(name: &str, needle: &str) -> bool {
306 if needle.is_empty() {
307 return true;
308 }
309 if name.is_ascii() && needle.is_ascii() {
310 return name
311 .as_bytes()
312 .windows(needle.len())
313 .any(|window| window.eq_ignore_ascii_case(needle.as_bytes()));
314 }
315 name.to_lowercase().contains(&needle.to_lowercase())
316}
317
318pub fn init_device_for_adapter(
328 index: usize,
329) -> Result<(
330 (wgpu::Device, wgpu::Queue),
331 wgpu::AdapterInfo,
332 crate::runtime::device::EnabledFeatures,
333)> {
334 super::device::wait_for_gpu(acquire_gpu_for_adapter(index))
335}
336
337pub(crate) fn init_device_for_adapter_identity(
344 identity: &AdapterIdentity,
345) -> Result<(
346 (wgpu::Device, wgpu::Queue),
347 wgpu::AdapterInfo,
348 crate::runtime::device::EnabledFeatures,
349)> {
350 super::device::wait_for_gpu(acquire_gpu_for_adapter_identity(identity))
351}
352
353async fn acquire_gpu_for_adapter_identity(
354 identity: &AdapterIdentity,
355) -> Result<(
356 (wgpu::Device, wgpu::Queue),
357 wgpu::AdapterInfo,
358 crate::runtime::device::EnabledFeatures,
359)> {
360 let instance = wgpu::Instance::default();
361 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
362 for adapter in &adapters {
363 let info = adapter.get_info();
364 if identity.matches(&info) {
365 if !crate::capabilities::is_real_gpu(&info) {
366 return Err(Error::Gpu {
367 message: format!(
368 "recovery target `{}` now reports device type {:?}, which is not a real GPU execution target. Fix: restore the original GPU adapter or construct a new backend for the changed adapter.",
369 info.name, info.device_type
370 ),
371 });
372 }
373 return super::device::request_device_for_adapter(adapter, "vyre device (recovered)")
374 .await;
375 }
376 }
377
378 let mut probed = Vec::new();
379 reserve_probe_vec(
380 &mut probed,
381 adapters.len(),
382 "adapter identity recovery probe report",
383 )?;
384 probed.extend(adapters.iter().map(|adapter| {
385 let info = adapter.get_info();
386 format!(
387 "{} ({:?}, backend={:?}, vendor={:08x}, device={:08x})",
388 info.name, info.device_type, info.backend, info.vendor, info.device
389 )
390 }));
391 Err(Error::Gpu {
392 message: format!(
393 "original recovery adapter was not found. Target: {:?}. Probed adapters: [{}]. Fix: restore the original GPU or create a new WgpuBackend for the available adapter.",
394 identity,
395 probed.join(", ")
396 ),
397 })
398}
399
400pub async fn acquire_gpu_for_adapter(
407 index: usize,
408) -> Result<(
409 (wgpu::Device, wgpu::Queue),
410 wgpu::AdapterInfo,
411 crate::runtime::device::EnabledFeatures,
412)> {
413 let instance = wgpu::Instance::default();
414 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
415 let adapter = adapters.get(index).ok_or_else(|| Error::Gpu {
416 message: format!(
417 "adapter index {index} out of range (saw {} adapters). Fix: call enumerate_adapters() first to see valid indices.",
418 adapters.len()
419 ),
420 })?;
421 let info = adapter.get_info();
422 if !crate::capabilities::is_real_gpu(&info) {
423 return Err(Error::Gpu {
424 message: format!(
425 "adapter index {index} resolved to `{}` with device type {:?}, which is not a real GPU execution target. Fix: choose a discrete, integrated, or virtual GPU adapter.",
426 info.name, info.device_type
427 ),
428 });
429 }
430 super::device::request_device_for_adapter(adapter, "vyre device (selected)").await
431}
432
433#[must_use]
441pub fn adapter_index_from_env() -> Result<Option<usize>> {
442 adapter_index_from_raw(std::env::var("VYRE_ADAPTER_INDEX").ok().as_deref())
443}
444
445fn adapter_index_from_raw(raw: Option<&str>) -> Result<Option<usize>> {
446 let Some(raw) = raw else {
447 return Ok(None);
448 };
449 raw.parse::<usize>().map(Some).map_err(|error| Error::Gpu {
450 message: format!(
451 "VYRE_ADAPTER_INDEX={raw:?} is not a valid adapter index: {error}. Fix: set VYRE_ADAPTER_INDEX to a non-negative integer from enumerate_adapters(), or unset it for automatic GPU selection."
452 ),
453 })
454}
455
456fn reserve_probe_vec<T>(vec: &mut Vec<T>, additional: usize, context: &'static str) -> Result<()> {
457 reserve_backend_vec(vec, additional, context).map_err(|error| Error::Gpu {
458 message: error.to_string(),
459 })
460}
461
462#[cfg(test)]
463
464mod tests {
465 use super::*;
466
467 #[test]
468 fn enumerate_adapters_finds_required_gpu() {
469 let adapters = enumerate_adapters();
470 assert_ne!(adapters.len(), 0,
471 "Fix: WGPU adapter enumeration returned no adapters on a GPU-required release host; repair driver/runtime configuration instead of accepting a CPU-only environment."
472 );
473 }
474
475 #[test]
476 fn criteria_high_perf_has_discrete_preset() {
477 let c = AdapterCriteria::high_performance();
478 assert_eq!(c.device_type, Some(wgpu::DeviceType::DiscreteGpu));
479 assert_eq!(c.power, Some(wgpu::PowerPreference::HighPerformance));
480 }
481
482 #[test]
483 fn criteria_low_power_has_integrated_preset() {
484 let c = AdapterCriteria::low_power();
485 assert_eq!(c.device_type, Some(wgpu::DeviceType::IntegratedGpu));
486 }
487
488 #[test]
489 fn env_override_parses_valid_index() {
490 assert_eq!(adapter_index_from_raw(Some("3")).unwrap(), Some(3));
491 }
492
493 #[test]
494 fn env_override_rejects_garbage() {
495 let error = adapter_index_from_raw(Some("not-a-number"))
496 .expect_err("invalid VYRE_ADAPTER_INDEX must error");
497 assert!(
498 error.to_string().contains("VYRE_ADAPTER_INDEX"),
499 "Fix: invalid adapter-index errors must name the misconfigured env var"
500 );
501 }
502
503 #[test]
504 fn selection_rejects_cpu_adapters_before_device_acquisition() {
505 let cpu_info = wgpu::AdapterInfo {
506 name: "llvmpipe".to_string(),
507 vendor: 0,
508 device: 0,
509 device_type: wgpu::DeviceType::Cpu,
510 driver: "software".to_string(),
511 driver_info: "cpu".to_string(),
512 backend: wgpu::Backend::Vulkan,
513 };
514 let gpu_info = wgpu::AdapterInfo {
515 name: "RTX 5090".to_string(),
516 vendor: 0x10de,
517 device: 0x2c02,
518 device_type: wgpu::DeviceType::DiscreteGpu,
519 driver: "nvidia".to_string(),
520 driver_info: "gpu".to_string(),
521 backend: wgpu::Backend::Vulkan,
522 };
523 let criteria = AdapterCriteria::default();
524
525 assert!(
526 !adapter_is_selectable(&cpu_info, &criteria),
527 "Fix: adapter selection must never return CPU/Other devices for later fallback handling."
528 );
529 assert!(adapter_is_selectable(&gpu_info, &criteria));
530 }
531
532 #[test]
533 fn adapter_identity_matches_every_recovery_field() {
534 let info = wgpu::AdapterInfo {
535 name: "gpu-a".to_string(),
536 vendor: 0x10de,
537 device: 0x2684,
538 device_type: wgpu::DeviceType::DiscreteGpu,
539 driver: "nvidia".to_string(),
540 driver_info: "driver-a".to_string(),
541 backend: wgpu::Backend::Vulkan,
542 };
543 let identity = AdapterIdentity::from_info(&info);
544 assert!(identity.matches(&info));
545
546 let mut changed = info.clone();
547 changed.device = 0x2685;
548 assert!(
549 !identity.matches(&changed),
550 "Fix: recovery must not silently bind to a different physical adapter."
551 );
552 }
553
554 #[test]
555 fn adapter_name_contains_matches_ascii_without_lowercase_in_hot_path() {
556 assert!(adapter_name_contains("NVIDIA GeForce RTX 5090", "rtx"));
557 assert!(adapter_name_contains("NVIDIA GeForce RTX 5090", "RTX"));
558 assert!(!adapter_name_contains("NVIDIA GeForce RTX 5090", "radeon"));
559 assert!(adapter_name_contains("Mötley GPU", "mötley"));
560 }
561
562 #[test]
563 fn production_selector_uses_fallible_probe_reservations() {
564 let production = include_str!("selector.rs")
565 .split("#[cfg(test)]")
566 .next()
567 .expect("Fix: selector production section should precede tests");
568
569 assert!(
570 !production.contains("Vec::with_capacity"),
571 "Fix: GPU probe paths must not use infallible capacity constructors."
572 );
573 assert!(
574 production.contains("reserve_probe_vec"),
575 "Fix: GPU probe metadata should reserve through the shared WGPU staging helper."
576 );
577 assert!(
578 !production.contains("info.name.to_lowercase()"),
579 "Fix: adapter matching must not allocate lowercase strings per adapter."
580 );
581 assert!(production.contains("adapter_name_contains"));
582 }
583}