git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit5e1a441c637b9ea8e059e496405ec7bd38bfc2a6
parent51d5f254ed
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 13:05
feat: an unmet CCE_VK_DEVICE preference says so

When the chosen device does not meet an explicit CCE_VK_DEVICE, print
once to stderr what was asked, what was taken and every device the
loader offered. The fallback was silent, and it renders exactly as the
asked-for device would: inside a cce-shadow the NVIDIA ICD does not load
without DISPLAY/XAUTHORITY, and "discrete" captures that were all on the
Intel GPU passed for NVIDIA ones (2026-10-02). A driver that failed to
load is in no list, so the line names VK_LOADER_DEBUG=error.

Also notes, in CLAUDE.md, the relief_shade test pinning that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md      | 16 +++++++++++--
 src/vk/core.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 86 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index a3bb90a..560cdf1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -442,8 +442,8 @@ between a plate and its carves, so two captures of "the same" pane can differ in
 grouped. And inside a `cce-shadow` session the NVIDIA ICD does not load at all unless
 the process can reach an X display (`DISPLAY=:0` and `XAUTHORITY=$HOME/.Xauthority`):
 `vk_icdGetInstanceProcAddr` fails, the loader skips the ICD, and `CCE_VK_DEVICE=discrete`
-falls back to the Intel device SILENTLY. Check `grep -c nvidia /proc/<pid>/maps` before
-believing a "discrete" shadow capture.
+fell back to the Intel device SILENTLY. It says so on stderr now (below, "Debug
+environment variables"); `grep -c nvidia /proc/<pid>/maps` is the check from outside.
 
 ### A row can open a submenu (since 2026-09-29)
 
@@ -1236,6 +1236,18 @@ All opt-in, all read once, all quiet when unset — set one and run any client.
 - `CCE_UI_MENU_POPUP=0` — keep the context menu in the window instead of its popup
   surface (see "The context menu draws in its own popup surface").
 - `CCE_VK_DEVICE=<substring>` — force a physical device; `CCE_VK_RT=0` disables ray tracing.
+  `integrated`, `discrete` or a name substring; any of them also lifts a session-wide ICD
+  pin (`VK_DRIVER_FILES`) for the process. **A preference the chosen device does not meet
+  is printed to stderr** (since 2026-10-02, once per process, `unmet_device_preference` in
+  `vk/core.rs`): what was asked, what was taken, and every device the loader offered — a
+  driver that failed to LOAD is in no list, which is the case the line points at
+  (`VK_LOADER_DEBUG=error` says why). Until then the fallback was silent, and a fallback
+  renders exactly as the asked-for device would, so nothing on screen gave it away.
+- The cce-ui suite reads `~/.config/cce` through the style registry, and LAZILY: a value
+  read before the first load and one read after come from two configurations. A test
+  asserting on shading numbers pins its inputs instead (`relief_shade`'s tests:
+  `pinned_light`, `pinned_finish`); `deeper_carve_shades_harder` failed run alone and
+  passed in the full suite until it did.
 - `CCE_FORCE_SCALE=<f>` — override HiDPI scale detection.
 - `CCE_FORCE_PPI=<f>` — pin the display metric (logical px per inch) regardless of what
   the outputs report; a headless shadow has no EDID and would run `assumed`. The live
diff --git a/src/vk/core.rs b/src/vk/core.rs
index 8fdd6ca..a8a08fa 100644
--- a/src/vk/core.rs
+++ b/src/vk/core.rs
@@ -225,6 +225,38 @@ impl std::fmt::Display for SurfaceLost {
 
 impl std::error::Error for SurfaceLost {}
 
+fn device_type_name(t: vk::PhysicalDeviceType) -> &'static str {
+    match t {
+        vk::PhysicalDeviceType::INTEGRATED_GPU => "integrated",
+        vk::PhysicalDeviceType::DISCRETE_GPU => "discrete",
+        vk::PhysicalDeviceType::VIRTUAL_GPU => "virtual",
+        vk::PhysicalDeviceType::CPU => "cpu",
+        _ => "other",
+    }
+}
+
+/// The warning for a `CCE_VK_DEVICE` preference the chosen device does not
+/// meet, or `None` when there was no explicit preference or it was met (a
+/// rank of 0 is a match, whatever the preference). `chosen` and `seen` are
+/// device descriptions, `seen` every one the loader offered.
+///
+/// A vendor whose driver failed to LOAD is in no list at all, which is the
+/// case worth naming: in a cce-shadow session the NVIDIA ICD will not load
+/// without an X display (`DISPLAY` and `XAUTHORITY`), and the loader says why
+/// only when asked (`VK_LOADER_DEBUG=error`).
+fn unmet_device_preference(pref: Option<&str>, chosen_rank: i32, chosen: &str, seen: &[String]) -> Option<String> {
+    let pref = pref?;
+    if chosen_rank == 0 {
+        return None;
+    }
+    Some(format!(
+        "cce-ui: CCE_VK_DEVICE={pref} is not met; using {chosen}. Devices the Vulkan loader offered: {}. \
+         A driver that failed to load is not among them (VK_LOADER_DEBUG=error says why; in a \
+         cce-shadow session the NVIDIA driver needs DISPLAY and XAUTHORITY).",
+        seen.join(", ")
+    ))
+}
+
 impl VkCore {
     /// A core bound to a Wayland surface: the returned `vk::SurfaceKHR` is
     /// created from the raw pointers and the chosen device supports presenting
@@ -341,6 +373,9 @@ impl VkCore {
         // on a dead display connection every device fails it, and "no
         // suitable device" would then misreport a lost surface.
         let mut support_error: Option<vk::Result> = None;
+        // Every device the loader offered, for the warning below when the
+        // preference cannot be met — including those that cannot draw here.
+        let mut seen: Vec<String> = Vec::new();
         for pd in instance
             .enumerate_physical_devices()
             .expect("No Vulkan physical devices")
@@ -359,6 +394,12 @@ impl VkCore {
                 };
                 (graphics && present).then_some(i as u32)
             });
+            {
+                let props = instance.get_physical_device_properties(pd);
+                let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
+                let usable = if family.is_some() { "" } else { ", cannot draw to this window" };
+                seen.push(format!("{name} ({}{usable})", device_type_name(props.device_type)));
+            }
             if let Some(family) = family {
                 let props = instance.get_physical_device_properties(pd);
                 let name = CStr::from_ptr(props.device_name.as_ptr())
@@ -398,13 +439,25 @@ impl VkCore {
             surface_loader.destroy_surface(surface, None);
             return Err(SurfaceLost { call: "vkGetPhysicalDeviceSurfaceSupportKHR", result });
         }
-        let (physical_device, queue_family, _) = *candidates
+        let (physical_device, queue_family, chosen_rank) = *candidates
             .first()
             .expect("No suitable Vulkan device found");
         {
             let props = instance.get_physical_device_properties(physical_device);
             let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
             log::info!("Vulkan device: {name}");
+            // An explicit preference that was not met falls back SILENTLY
+            // otherwise — and a fallback renders as the device asked for
+            // would, so nothing on screen says it happened (2026-10-02: hours
+            // of "discrete" shadow captures that were all on the Intel GPU).
+            // stderr, not `log`: most clients init no logger.
+            let chosen = format!("{name} ({})", device_type_name(props.device_type));
+            if let Some(msg) = unmet_device_preference(device_pref.as_deref(), chosen_rank, &chosen, &seen) {
+                static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
+                if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
+                    eprintln!("{msg}");
+                }
+            }
         }
         let min_uniform_align = instance
             .get_physical_device_properties(physical_device)
@@ -583,3 +636,21 @@ impl Drop for VkCore {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// An explicit preference that was not met says so; one that was met, or
+    /// no preference at all, says nothing.
+    #[test]
+    fn an_unmet_device_preference_is_reported() {
+        let seen = vec!["Intel(R) Iris(R) Xe Graphics (integrated)".to_string()];
+        let msg = unmet_device_preference(Some("discrete"), 1, &seen[0], &seen).expect("unmet");
+        assert!(msg.contains("CCE_VK_DEVICE=discrete is not met"), "{msg}");
+        assert!(msg.contains("using Intel(R) Iris(R) Xe Graphics (integrated)"), "{msg}");
+        assert!(msg.contains("DISPLAY and XAUTHORITY"), "{msg}");
+        assert_eq!(unmet_device_preference(Some("discrete"), 0, &seen[0], &seen), None);
+        assert_eq!(unmet_device_preference(None, 1, &seen[0], &seen), None);
+    }
+}