1#[cfg(target_os = "linux")]
37use std::ffi::{c_char, c_int, c_void, CStr};
38
39#[derive(Debug, Default, Clone, PartialEq, Eq)]
43pub struct GpuApis {
44 pub vulkan: Option<String>,
47 pub opengl: Option<String>,
50 pub opencl: Option<String>,
52}
53
54pub fn format_vulkan_version(packed: u32) -> String {
60 let major = (packed >> 22) & 0x7F;
61 let minor = (packed >> 12) & 0x3FF;
62 let patch = packed & 0xFFF;
63 format!("{major}.{minor}.{patch}")
64}
65
66pub fn device_type_rank(device_type: u32) -> u8 {
74 match device_type {
75 2 => 0, 1 => 1, 3 => 2, 4 => 4, _ => 3, }
81}
82
83pub fn format_vulkan(version: &str, driver_name: &str, driver_info: &str) -> String {
89 match (driver_name.trim(), driver_info.trim()) {
90 ("", _) => version.to_string(),
91 (name, "") => format!("{version} - {name}"),
92 (name, info) => format!("{version} - {name} [{info}]"),
93 }
94}
95
96pub fn format_opencl(version: &str, platform: &str, device: Option<&str>) -> String {
102 let version = version
105 .trim()
106 .strip_prefix("OpenCL ")
107 .unwrap_or(version.trim())
108 .trim();
109 let platform = platform.trim();
110 match device {
111 Some(d) if !d.trim().is_empty() => {
112 if platform.is_empty() {
113 format!("{version} ({})", d.trim())
114 } else {
115 format!("{version} - {platform} ({})", d.trim())
116 }
117 }
118 _ => {
119 if platform.is_empty() {
120 format!("{version} (no device enabled)")
121 } else {
122 format!("{version} - {platform} (no device enabled)")
123 }
124 }
125 }
126}
127
128pub fn shorten_device_name(name: &str) -> String {
136 match name.find(" (") {
137 Some(i) => name[..i].trim().to_string(),
138 None => name.trim().to_string(),
139 }
140}
141
142pub fn cstr_field(buf: &[u8]) -> String {
147 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
148 String::from_utf8_lossy(&buf[..end]).into_owned()
149}
150
151#[cfg(target_os = "linux")]
156mod dl {
157 use super::*;
158
159 extern "C" {
160 pub fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
161 pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
162 pub fn dlclose(handle: *mut c_void) -> c_int;
163 }
164 pub const RTLD_NOW: c_int = 2;
165 pub const RTLD_LOCAL: c_int = 0;
166
167 pub fn open(soname: &CStr) -> Option<*mut c_void> {
172 let h = unsafe { dlopen(soname.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
175 (!h.is_null()).then_some(h)
176 }
177
178 pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
180 let p = unsafe { dlsym(handle, name.as_ptr()) };
183 (!p.is_null()).then_some(p)
184 }
185
186 pub fn close(handle: *mut c_void) {
188 unsafe {
190 dlclose(handle);
191 }
192 }
193}
194
195#[cfg(target_os = "linux")]
196mod vulkan {
197 use super::dl;
198 use super::*;
199
200 const VK_STRUCTURE_TYPE_APPLICATION_INFO: u32 = 0;
201 const VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO: u32 = 1;
202 const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2: u32 = 1000059001;
203 const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES: u32 = 1000196000;
204
205 const PROPS2_BODY: usize = 16;
207 const OFF_API_VERSION: usize = 0;
209 const OFF_DEVICE_TYPE: usize = 16;
210 const OFF_DEVICE_NAME: usize = 20;
211 const PROPS_BUF: usize = 1024;
216
217 const OFF_DRIVER_NAME: usize = 20;
219 const OFF_DRIVER_INFO: usize = 276;
220 const DRIVER_BUF: usize = 560;
221 const VK_MAX_NAME: usize = 256;
222
223 #[repr(C)]
224 struct AppInfo {
225 s_type: u32,
226 p_next: *const c_void,
227 app_name: *const c_char,
228 app_version: u32,
229 engine_name: *const c_char,
230 engine_version: u32,
231 api_version: u32,
232 }
233
234 #[repr(C)]
235 struct InstanceCreateInfo {
236 s_type: u32,
237 p_next: *const c_void,
238 flags: u32,
239 app_info: *const AppInfo,
240 layer_count: u32,
241 layer_names: *const *const c_char,
242 ext_count: u32,
243 ext_names: *const *const c_char,
244 }
245
246 type VkCreateInstance =
247 unsafe extern "C" fn(*const InstanceCreateInfo, *const c_void, *mut *mut c_void) -> i32;
248 type VkDestroyInstance = unsafe extern "C" fn(*mut c_void, *const c_void);
249 type VkEnumeratePhysicalDevices =
250 unsafe extern "C" fn(*mut c_void, *mut u32, *mut *mut c_void) -> i32;
251 type VkGetPhysicalDeviceProperties2 = unsafe extern "C" fn(*mut c_void, *mut c_void);
252 type VkGetInstanceProcAddr = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
253
254 pub fn detect() -> Option<String> {
259 let lib = dl::open(c"libvulkan.so.1")?;
260 let result = detect_with(lib);
261 dl::close(lib);
262 result
263 }
264
265 fn detect_with(lib: *mut c_void) -> Option<String> {
266 let create = dl::sym(lib, c"vkCreateInstance")?;
267 let gipa = dl::sym(lib, c"vkGetInstanceProcAddr")?;
268
269 unsafe {
273 let create: VkCreateInstance = std::mem::transmute(create);
274 let gipa: VkGetInstanceProcAddr = std::mem::transmute(gipa);
275
276 let app = AppInfo {
277 s_type: VK_STRUCTURE_TYPE_APPLICATION_INFO,
278 p_next: std::ptr::null(),
279 app_name: c"retch".as_ptr(),
280 app_version: 0,
281 engine_name: std::ptr::null(),
282 engine_version: 0,
283 api_version: (1 << 22) | (2 << 12),
289 };
290 let ci = InstanceCreateInfo {
291 s_type: VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
292 p_next: std::ptr::null(),
293 flags: 0,
294 app_info: &app,
295 layer_count: 0,
296 layer_names: std::ptr::null(),
297 ext_count: 0,
298 ext_names: std::ptr::null(),
299 };
300
301 let mut instance: *mut c_void = std::ptr::null_mut();
302 if create(&ci, std::ptr::null(), &mut instance) != 0 || instance.is_null() {
303 return None;
304 }
305
306 let out = read_best_device(instance, gipa);
307
308 if let Some(p) = dl::sym(lib, c"vkDestroyInstance") {
309 let destroy: VkDestroyInstance = std::mem::transmute(p);
310 destroy(instance, std::ptr::null());
311 }
312 out
313 }
314 }
315
316 unsafe fn read_best_device(
319 instance: *mut c_void,
320 gipa: VkGetInstanceProcAddr,
321 ) -> Option<String> {
322 let enum_ptr = gipa(instance, c"vkEnumeratePhysicalDevices".as_ptr());
323 let props_ptr = gipa(instance, c"vkGetPhysicalDeviceProperties2".as_ptr());
324 if enum_ptr.is_null() || props_ptr.is_null() {
325 return None;
326 }
327 let enumerate: VkEnumeratePhysicalDevices = std::mem::transmute(enum_ptr);
328 let get_props2: VkGetPhysicalDeviceProperties2 = std::mem::transmute(props_ptr);
329
330 let mut count: u32 = 0;
331 if enumerate(instance, &mut count, std::ptr::null_mut()) != 0 || count == 0 {
332 return None;
333 }
334 let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
335 if enumerate(instance, &mut count, devices.as_mut_ptr()) != 0 {
336 return None;
337 }
338
339 let mut best: Option<(u8, String)> = None;
340 for device in devices.iter().take(count as usize) {
341 let mut driver = vec![0u8; DRIVER_BUF];
342 driver[0..4].copy_from_slice(
343 &VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES.to_ne_bytes(),
344 );
345 let mut props = vec![0u8; PROPS2_BODY + PROPS_BUF];
346 props[0..4]
347 .copy_from_slice(&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2.to_ne_bytes());
348 let chain = driver.as_mut_ptr() as usize;
349 props[8..16].copy_from_slice(&chain.to_ne_bytes());
350
351 get_props2(*device, props.as_mut_ptr() as *mut c_void);
352
353 let at = |off: usize| -> u32 {
354 let s = PROPS2_BODY + off;
355 u32::from_ne_bytes(props[s..s + 4].try_into().unwrap_or([0; 4]))
356 };
357 let api = at(OFF_API_VERSION);
358 let dtype = at(OFF_DEVICE_TYPE);
359 let name_start = PROPS2_BODY + OFF_DEVICE_NAME;
360 let _device_name = cstr_field(&props[name_start..name_start + VK_MAX_NAME]);
361
362 let driver_name = cstr_field(&driver[OFF_DRIVER_NAME..OFF_DRIVER_NAME + VK_MAX_NAME]);
363 let driver_info = cstr_field(&driver[OFF_DRIVER_INFO..OFF_DRIVER_INFO + VK_MAX_NAME]);
364
365 let rank = device_type_rank(dtype);
366 let rendered = format_vulkan(&format_vulkan_version(api), &driver_name, &driver_info);
367 if best.as_ref().is_none_or(|(r, _)| rank < *r) {
368 best = Some((rank, rendered));
369 }
370 }
371 best.map(|(_, s)| s)
372 }
373}
374
375#[cfg(target_os = "linux")]
376mod opengl {
377 use super::dl;
378 use super::*;
379
380 const EGL_OPENGL_API: u32 = 0x30A2;
381 const EGL_NONE: i32 = 0x3038;
382 const EGL_SURFACE_TYPE: i32 = 0x3033;
383 const EGL_PBUFFER_BIT: i32 = 0x0001;
384 const EGL_RENDERABLE_TYPE: i32 = 0x3040;
385 const EGL_OPENGL_BIT: i32 = 0x0008;
386 const GL_VERSION: u32 = 0x1F02;
387
388 type EglGetDisplay = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
389 type EglInitialize = unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) -> u32;
390 type EglBindApi = unsafe extern "C" fn(u32) -> u32;
391 type EglChooseConfig =
392 unsafe extern "C" fn(*mut c_void, *const i32, *mut *mut c_void, i32, *mut i32) -> u32;
393 type EglCreateContext =
394 unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i32) -> *mut c_void;
395 type EglMakeCurrent =
396 unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> u32;
397 type EglGetProcAddress = unsafe extern "C" fn(*const c_char) -> *mut c_void;
398 type EglTerminate = unsafe extern "C" fn(*mut c_void) -> u32;
399 type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
400
401 pub fn detect() -> Option<String> {
413 let lib = dl::open(c"libEGL.so.1")?;
414 let out = detect_with(lib);
415 dl::close(lib);
416 out
417 }
418
419 fn detect_with(lib: *mut c_void) -> Option<String> {
420 let get_display = dl::sym(lib, c"eglGetDisplay")?;
421 let initialize = dl::sym(lib, c"eglInitialize")?;
422 let bind_api = dl::sym(lib, c"eglBindAPI")?;
423 let choose = dl::sym(lib, c"eglChooseConfig")?;
424 let create_context = dl::sym(lib, c"eglCreateContext")?;
425 let make_current = dl::sym(lib, c"eglMakeCurrent")?;
426 let get_proc = dl::sym(lib, c"eglGetProcAddress")?;
427
428 unsafe {
432 let get_display: EglGetDisplay = std::mem::transmute(get_display);
433 let initialize: EglInitialize = std::mem::transmute(initialize);
434 let bind_api: EglBindApi = std::mem::transmute(bind_api);
435 let choose: EglChooseConfig = std::mem::transmute(choose);
436 let create_context: EglCreateContext = std::mem::transmute(create_context);
437 let make_current: EglMakeCurrent = std::mem::transmute(make_current);
438 let get_proc: EglGetProcAddress = std::mem::transmute(get_proc);
439
440 let display = get_display(std::ptr::null_mut());
442 if display.is_null() {
443 return None;
444 }
445 let (mut major, mut minor) = (0i32, 0i32);
446 if initialize(display, &mut major, &mut minor) == 0 {
447 return None;
448 }
449 if bind_api(EGL_OPENGL_API) == 0 {
452 terminate(lib, display);
453 return None;
454 }
455
456 let attrs = [
457 EGL_SURFACE_TYPE,
458 EGL_PBUFFER_BIT,
459 EGL_RENDERABLE_TYPE,
460 EGL_OPENGL_BIT,
461 EGL_NONE,
462 ];
463 let mut config: *mut c_void = std::ptr::null_mut();
464 let mut configs = 0i32;
465 if choose(display, attrs.as_ptr(), &mut config, 1, &mut configs) == 0 || configs == 0 {
466 terminate(lib, display);
467 return None;
468 }
469 let context = create_context(display, config, std::ptr::null_mut(), std::ptr::null());
470 if context.is_null() {
471 terminate(lib, display);
472 return None;
473 }
474 if make_current(display, std::ptr::null_mut(), std::ptr::null_mut(), context) == 0 {
475 terminate(lib, display);
476 return None;
477 }
478 let gl_get_string = get_proc(c"glGetString".as_ptr());
479 let version = if gl_get_string.is_null() {
480 None
481 } else {
482 let gl_get_string: GlGetString = std::mem::transmute(gl_get_string);
483 let p = gl_get_string(GL_VERSION);
484 if p.is_null() {
485 None
486 } else {
487 Some(CStr::from_ptr(p).to_string_lossy().into_owned())
488 }
489 };
490 terminate(lib, display);
491 version.filter(|v| !v.trim().is_empty())
492 }
493 }
494
495 unsafe fn terminate(lib: *mut c_void, display: *mut c_void) {
499 if let Some(p) = dl::sym(lib, c"eglTerminate") {
500 let terminate: EglTerminate = std::mem::transmute(p);
501 terminate(display);
502 }
503 }
504}
505
506#[cfg(target_os = "linux")]
507mod opencl {
508 use super::dl;
509 use super::*;
510
511 const CL_PLATFORM_VERSION: u32 = 0x0901;
512 const CL_PLATFORM_NAME: u32 = 0x0902;
513 const CL_DEVICE_TYPE_ALL: u64 = 0xFFFF_FFFF;
514 const CL_DEVICE_NAME: u32 = 0x102B;
515
516 type ClGetPlatformIDs = unsafe extern "C" fn(u32, *mut *mut c_void, *mut u32) -> i32;
517 type ClGetPlatformInfo =
518 unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
519 type ClGetDeviceIDs =
520 unsafe extern "C" fn(*mut c_void, u64, u32, *mut *mut c_void, *mut u32) -> i32;
521 type ClGetDeviceInfo =
522 unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
523
524 extern "C" {
525 fn dup(oldfd: c_int) -> c_int;
526 fn dup2(oldfd: c_int, newfd: c_int) -> c_int;
527 fn close(fd: c_int) -> c_int;
528 fn open(path: *const c_char, flags: c_int) -> c_int;
529 }
530 const STDERR_FILENO: c_int = 2;
531 const O_WRONLY: c_int = 1;
532
533 struct SuppressStderr {
550 saved: c_int,
551 }
552
553 impl SuppressStderr {
554 fn new() -> Option<Self> {
555 unsafe {
558 let saved = dup(STDERR_FILENO);
559 if saved < 0 {
560 return None;
561 }
562 let devnull = open(c"/dev/null".as_ptr(), O_WRONLY);
563 if devnull < 0 {
564 close(saved);
565 return None;
566 }
567 dup2(devnull, STDERR_FILENO);
568 close(devnull);
569 Some(Self { saved })
570 }
571 }
572 }
573
574 impl Drop for SuppressStderr {
575 fn drop(&mut self) {
576 unsafe {
578 dup2(self.saved, STDERR_FILENO);
579 close(self.saved);
580 }
581 }
582 }
583
584 pub fn detect() -> Option<String> {
589 let _quiet = SuppressStderr::new();
592 let lib = dl::open(c"libOpenCL.so.1")?;
593 let out = detect_with(lib);
594 dl::close(lib);
595 out
596 }
597
598 fn detect_with(lib: *mut c_void) -> Option<String> {
599 let get_platform_ids = dl::sym(lib, c"clGetPlatformIDs")?;
600 let get_platform_info = dl::sym(lib, c"clGetPlatformInfo")?;
601
602 unsafe {
605 let get_platform_ids: ClGetPlatformIDs = std::mem::transmute(get_platform_ids);
606 let get_platform_info: ClGetPlatformInfo = std::mem::transmute(get_platform_info);
607
608 let mut count: u32 = 0;
609 if get_platform_ids(0, std::ptr::null_mut(), &mut count) != 0 || count == 0 {
610 return None;
611 }
612 let mut platforms = vec![std::ptr::null_mut::<c_void>(); count as usize];
613 if get_platform_ids(count, platforms.as_mut_ptr(), std::ptr::null_mut()) != 0 {
614 return None;
615 }
616 let platform = *platforms.first()?;
617
618 let version = query(get_platform_info, platform, CL_PLATFORM_VERSION)?;
619 let name = query(get_platform_info, platform, CL_PLATFORM_NAME).unwrap_or_default();
620
621 let device = dl::sym(lib, c"clGetDeviceIDs")
622 .zip(dl::sym(lib, c"clGetDeviceInfo"))
623 .and_then(|(ids, info)| first_device_name(platform, ids, info))
624 .map(|n| shorten_device_name(&n));
625
626 Some(format_opencl(&version, &name, device.as_deref()))
627 }
628 }
629
630 unsafe fn query(f: ClGetPlatformInfo, obj: *mut c_void, param: u32) -> Option<String> {
634 let mut size: usize = 0;
635 if f(obj, param, 0, std::ptr::null_mut(), &mut size) != 0 || size == 0 {
636 return None;
637 }
638 let mut buf = vec![0u8; size];
639 if f(
640 obj,
641 param,
642 size,
643 buf.as_mut_ptr() as *mut c_void,
644 std::ptr::null_mut(),
645 ) != 0
646 {
647 return None;
648 }
649 let s = cstr_field(&buf);
650 (!s.trim().is_empty()).then(|| s.trim().to_string())
651 }
652
653 unsafe fn first_device_name(
658 platform: *mut c_void,
659 ids: *mut c_void,
660 info: *mut c_void,
661 ) -> Option<String> {
662 let get_device_ids: ClGetDeviceIDs = std::mem::transmute(ids);
663 let get_device_info: ClGetDeviceInfo = std::mem::transmute(info);
664
665 let mut count: u32 = 0;
666 if get_device_ids(
669 platform,
670 CL_DEVICE_TYPE_ALL,
671 0,
672 std::ptr::null_mut(),
673 &mut count,
674 ) != 0
675 || count == 0
676 {
677 return None;
678 }
679 let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
680 if get_device_ids(
681 platform,
682 CL_DEVICE_TYPE_ALL,
683 count,
684 devices.as_mut_ptr(),
685 std::ptr::null_mut(),
686 ) != 0
687 {
688 return None;
689 }
690 let device = *devices.first()?;
691 let mut size: usize = 0;
692 if get_device_info(device, CL_DEVICE_NAME, 0, std::ptr::null_mut(), &mut size) != 0
693 || size == 0
694 {
695 return None;
696 }
697 let mut buf = vec![0u8; size];
698 if get_device_info(
699 device,
700 CL_DEVICE_NAME,
701 size,
702 buf.as_mut_ptr() as *mut c_void,
703 std::ptr::null_mut(),
704 ) != 0
705 {
706 return None;
707 }
708 let s = cstr_field(&buf);
709 (!s.trim().is_empty()).then(|| s.trim().to_string())
710 }
711}
712
713#[cfg(target_os = "linux")]
719pub fn detect_gpu_apis() -> GpuApis {
720 GpuApis {
721 vulkan: vulkan::detect(),
722 opengl: opengl::detect(),
723 opencl: opencl::detect(),
724 }
725}
726
727#[cfg(not(target_os = "linux"))]
729pub fn detect_gpu_apis() -> GpuApis {
730 GpuApis::default()
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn test_format_vulkan_version_decodes_packed_fields() {
739 assert_eq!(format_vulkan_version(0x0040_4155), "1.4.341");
741 assert_eq!(format_vulkan_version(1 << 22), "1.0.0");
743 assert_eq!(format_vulkan_version((1 << 22) | (2 << 12)), "1.2.0");
744 assert_eq!(
745 format_vulkan_version((1 << 22) | (3 << 12) | 290),
746 "1.3.290"
747 );
748 }
749
750 #[test]
751 fn test_format_vulkan_version_ignores_variant_bits() {
752 let with_variant = (1u32 << 29) | (1 << 22) | (4 << 12) | 354;
755 assert_eq!(format_vulkan_version(with_variant), "1.4.354");
756 }
757
758 #[test]
759 fn test_device_type_rank_prefers_real_gpu_over_software() {
760 assert!(device_type_rank(1) < device_type_rank(4));
763 assert!(device_type_rank(2) < device_type_rank(1)); assert!(device_type_rank(3) < device_type_rank(4)); assert!(device_type_rank(0) < device_type_rank(4)); }
767
768 #[test]
769 fn test_format_vulkan_handles_unfilled_driver_chain() {
770 assert_eq!(format_vulkan("1.4.354", "", ""), "1.4.354");
773 assert_eq!(format_vulkan("1.4.354", "radv", ""), "1.4.354 - radv");
774 assert_eq!(
775 format_vulkan("1.4.354", "radv", "Mesa 26.1.8"),
776 "1.4.354 - radv [Mesa 26.1.8]"
777 );
778 }
779
780 #[test]
781 fn test_format_opencl_distinguishes_inert_platform_from_working_one() {
782 assert_eq!(
785 format_opencl("OpenCL 3.0", "rusticl", None),
786 "3.0 - rusticl (no device enabled)"
787 );
788 assert_eq!(
789 format_opencl("OpenCL 3.0", "rusticl", Some("AMD Radeon 780M Graphics")),
790 "3.0 - rusticl (AMD Radeon 780M Graphics)"
791 );
792 assert_eq!(
794 format_opencl("OpenCL 3.0", "rusticl", Some(" ")),
795 "3.0 - rusticl (no device enabled)"
796 );
797 }
798
799 #[test]
800 fn test_format_opencl_without_platform_name() {
801 assert_eq!(
802 format_opencl("OpenCL 1.2", "", None),
803 "1.2 (no device enabled)"
804 );
805 assert_eq!(format_opencl("OpenCL 1.2", "", Some("GPU")), "1.2 (GPU)");
806 assert_eq!(format_opencl("3.0", "x", Some("GPU")), "3.0 - x (GPU)");
809 }
810
811 #[test]
812 fn test_shorten_device_name_drops_the_driver_descriptor() {
813 assert_eq!(
815 shorten_device_name(
816 "AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)"
817 ),
818 "AMD Radeon 780M Graphics"
819 );
820 assert_eq!(
822 shorten_device_name("NVIDIA GeForce RTX 4090"),
823 "NVIDIA GeForce RTX 4090"
824 );
825 assert_eq!(
827 shorten_device_name("Intel(R) Arc(TM) A770"),
828 "Intel(R) Arc(TM) A770"
829 );
830 }
831
832 #[test]
833 fn test_cstr_field_stops_at_nul() {
834 let mut buf = [0u8; 16];
835 buf[..4].copy_from_slice(b"radv");
836 assert_eq!(cstr_field(&buf), "radv");
837 assert_eq!(cstr_field(&[0u8; 16]), "");
839 assert_eq!(cstr_field(b"abcd"), "abcd");
841 }
842}