use crate::{VulkanHost, VulkanVersion};
use std::path::Path;
const ICD_DIRS: [&str; 2] = ["/usr/local/share/vulkan/icd.d", "/usr/share/vulkan/icd.d"];
const LOADER_SONAMES: [&str; 2] = ["libvulkan.so.1", "libvulkan.so"];
const LIB_DIRS: [&str; 4] = [
"/usr/lib/x86_64-linux-gnu",
"/usr/lib64",
"/usr/lib",
"/usr/local/lib",
];
fn parse_icd_api_version(content: &str) -> Option<VulkanVersion> {
let value: serde_json::Value = serde_json::from_str(content).ok()?;
let text = value.get("ICD")?.get("api_version")?.as_str()?;
let (major, minor, patch) = crate::parse_dotted_version(text)?;
Some(VulkanVersion {
major,
minor,
patch,
})
}
fn loader_in<P: AsRef<Path>>(dirs: &[P]) -> bool {
dirs.iter()
.flat_map(|dir| LOADER_SONAMES.iter().map(move |so| dir.as_ref().join(so)))
.any(|path| path.exists())
}
fn highest_api_version<P: AsRef<Path>>(dirs: &[P]) -> Option<VulkanVersion> {
dirs.iter()
.filter_map(|dir| std::fs::read_dir(dir).ok())
.flatten()
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|e| e == "json"))
.filter_map(|entry| std::fs::read_to_string(entry.path()).ok())
.filter_map(|content| parse_icd_api_version(&content))
.max()
}
fn host_in<L: AsRef<Path>, I: AsRef<Path>>(lib_dirs: &[L], icd_dirs: &[I]) -> Option<VulkanHost> {
if !loader_in(lib_dirs) {
return None;
}
let api_version = highest_api_version(icd_dirs)?;
Some(VulkanHost { api_version })
}
pub(crate) fn host() -> Option<VulkanHost> {
host_in(&LIB_DIRS, &ICD_DIRS)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
const RADEON: &str = r#"{"file_format_version":"1.0.0",
"ICD":{"library_path":"/usr/lib/libvulkan_radeon.so",
"api_version":"1.3.280"}}"#;
const LAVAPIPE: &str = r#"{"file_format_version":"1.0.1",
"ICD":{"library_path":"/usr/lib/libvulkan_lvp.so",
"api_version":"1.3.255"}}"#;
struct TempTree(PathBuf);
impl TempTree {
fn new(label: &str) -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let nth = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"gpu-probe-vulkan-{}-{label}-{nth}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("scratch directory is creatable");
Self(path)
}
fn with(&self, name: &str, content: &str) -> &Self {
std::fs::write(self.0.join(name), content).expect("fixture file is writable");
self
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempTree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn parses_an_icd_manifest_api_version() {
let icd = r#"{"file_format_version":"1.0.0",
"ICD":{"library_path":"libvulkan_radeon.so",
"api_version":"1.3.280"}}"#;
assert_eq!(
parse_icd_api_version(icd),
Some(VulkanVersion::new(1, 3, 280))
);
assert_eq!(
parse_icd_api_version(LAVAPIPE),
Some(VulkanVersion::new(1, 3, 255))
);
let nvidia = r#"{"file_format_version":"1.0.1",
"ICD":{"library_path":"libGLX_nvidia.so.0",
"api_version":"1.3.277",
"is_portability_driver":false}}"#;
assert_eq!(
parse_icd_api_version(nvidia),
Some(VulkanVersion::new(1, 3, 277))
);
}
#[test]
fn rejects_an_icd_manifest_without_an_api_version() {
let icd = r#"{"ICD":{"library_path":"libvulkan_radeon.so"}}"#;
assert_eq!(parse_icd_api_version(icd), None);
assert_eq!(parse_icd_api_version("not json"), None);
assert_eq!(parse_icd_api_version(""), None);
assert_eq!(parse_icd_api_version("{}"), None);
assert_eq!(parse_icd_api_version(r#"{"api_version":"1.3.280"}"#), None);
assert_eq!(
parse_icd_api_version(r#"{"ICD":{"api_version":1.3}}"#),
None
);
assert_eq!(parse_icd_api_version(r#"{"ICD":"1.3.280"}"#), None);
assert_eq!(
parse_icd_api_version(r#"{"ICD":[{"api_version":"1.3.0"}]}"#),
None
);
}
#[test]
fn rejects_manifests_whose_version_is_not_a_version() {
let with = |version: &str| {
parse_icd_api_version(&format!(r#"{{"ICD":{{"api_version":"{version}"}}}}"#))
};
assert_eq!(with("1.2"), Some(VulkanVersion::new(1, 2, 0)));
assert_eq!(with("1"), None, "a bare major is not a version");
assert_eq!(with(""), None);
assert_eq!(with("one.three.zero"), None);
assert_eq!(with("1.3.x"), None);
}
#[test]
fn versions_order_major_first() {
assert!(VulkanVersion::new(1, 3, 0) > VulkanVersion::new(1, 2, 300));
assert!(VulkanVersion::new(2, 0, 0) > VulkanVersion::new(1, 9, 9));
}
#[test]
fn loader_is_found_under_either_soname() {
let versioned = TempTree::new("loader-soname-1");
versioned.with("libvulkan.so.1", "");
assert!(loader_in(&[versioned.path()]));
let unversioned = TempTree::new("loader-soname-dev");
unversioned.with("libvulkan.so", "");
assert!(loader_in(&[unversioned.path()]));
let empty = TempTree::new("loader-empty-first");
assert!(loader_in(&[empty.path(), versioned.path()]));
}
#[test]
fn a_host_without_the_loader_reports_no_install() {
let empty = TempTree::new("loader-absent");
assert!(!loader_in(&[empty.path()]));
assert!(!loader_in(&[Path::new("/nonexistent-vulkan-libdir")]));
assert!(!loader_in::<&Path>(&[]));
let unlinked = TempTree::new("loader-unlinked");
unlinked.with("libvulkan.so.1.3.280", "");
assert!(!loader_in(&[unlinked.path()]));
}
#[cfg(unix)]
#[test]
fn a_dangling_loader_symlink_is_not_an_install() {
let tree = TempTree::new("loader-dangling");
std::os::unix::fs::symlink("libvulkan.so.1.3.280", tree.path().join("libvulkan.so.1"))
.expect("symlink is creatable");
assert!(!loader_in(&[tree.path()]));
tree.with("libvulkan.so.1.3.280", "");
assert!(loader_in(&[tree.path()]), "the same link now resolves");
}
#[test]
fn highest_api_version_wins_across_manifests() {
let icd = TempTree::new("icd-highest");
icd.with("lvp_icd.x86_64.json", LAVAPIPE)
.with("radeon_icd.x86_64.json", RADEON);
assert_eq!(
highest_api_version(&[icd.path()]),
Some(VulkanVersion::new(1, 3, 280))
);
let local = TempTree::new("icd-local");
local.with("lvp_icd.x86_64.json", LAVAPIPE);
let shared = TempTree::new("icd-shared");
shared.with("radeon_icd.x86_64.json", RADEON);
assert_eq!(
highest_api_version(&[local.path(), shared.path()]),
Some(VulkanVersion::new(1, 3, 280))
);
assert_eq!(
highest_api_version(&[shared.path(), local.path()]),
Some(VulkanVersion::new(1, 3, 280)),
"directory order must not change the answer",
);
}
#[test]
fn only_json_manifests_are_read() {
let icd = TempTree::new("icd-extensions");
icd.with("lvp_icd.x86_64.json", LAVAPIPE)
.with("radeon_icd.x86_64.json.disabled", RADEON)
.with("radeon_icd.x86_64.json.bak", RADEON)
.with("notes.txt", RADEON)
.with("README", RADEON);
assert_eq!(
highest_api_version(&[icd.path()]),
Some(VulkanVersion::new(1, 3, 255)),
"only the .json manifest counts, so lavapipe's version stands",
);
}
#[test]
fn a_malformed_manifest_does_not_hide_a_good_one() {
let icd = TempTree::new("icd-malformed");
icd.with("broken_icd.json", "{ not json")
.with("empty_icd.json", "")
.with("versionless_icd.json", r#"{"ICD":{"library_path":"x.so"}}"#)
.with("radeon_icd.x86_64.json", RADEON);
assert_eq!(
highest_api_version(&[icd.path()]),
Some(VulkanVersion::new(1, 3, 280))
);
}
#[test]
fn missing_or_empty_icd_directories_report_nothing() {
let empty = TempTree::new("icd-empty");
assert_eq!(highest_api_version(&[empty.path()]), None);
assert_eq!(
highest_api_version(&[Path::new("/nonexistent-vulkan-icd-dir")]),
None,
"an absent directory is skipped, not an error",
);
assert_eq!(highest_api_version::<&Path>(&[]), None);
}
#[test]
fn both_halves_are_required() {
let lib = TempTree::new("host-lib");
lib.with("libvulkan.so.1", "");
let icd = TempTree::new("host-icd");
icd.with("radeon_icd.x86_64.json", RADEON);
let empty = TempTree::new("host-empty");
assert_eq!(
host_in(&[lib.path()], &[icd.path()]),
Some(VulkanHost {
api_version: VulkanVersion::new(1, 3, 280)
})
);
assert_eq!(
host_in(&[empty.path()], &[icd.path()]),
None,
"manifests describe drivers nothing can dispatch to without a loader",
);
assert_eq!(
host_in(&[lib.path()], &[empty.path()]),
None,
"a loader with no ICD advertises no version to report",
);
}
#[test]
fn host_lookup_never_panics() {
if let Some(v) = host() {
assert!(v.api_version.major > 0);
}
}
}