//! Why a GPU test segfaults *after* it has passed, and what stops it. //! //! Measured here 2026-09-08, on this VM's Venus adapter. Destroying the //! last `VkInstance` makes the Vulkan loader `dlclose` the ICD; Mesa's //! ICD (`/usr/lib/libvulkan_virtio.so`) registers a `pthread_key_create` //! destructor pointing into its own text and is not linked `-z nodelete`, //! so glibc calls that destructor through unmapped memory when the thread //! that used Vulkan exits. libtest runs every `#[test]` on a spawned //! thread, which is why it looked like "wgpu crashes on drop": the drop //! itself completes, and the crash lands as the thread unwinds. //! //! The four modes are the experiment, and each is one variable: //! //! | mode | what it does | 2026-09-08 | //! |---|---|---| //! | `main` | wgpu instance + device on the main thread, dropped | exits 0 | //! | `thread` | the same on a spawned thread | **SIGSEGV** | //! | `keep` | the same, but the instance is never dropped | exits 0 | //! | `raw` | raw Vulkan (`ash`), instance + device, spawned thread | **SIGSEGV** | //! //! `raw` is the one that says whose bug it is: no wgpu is involved, so //! there is nothing for wgpu or a caller to fix in its drop order. `keep` //! is the fix -- hold one `wgpu::Instance` for the process, which is what //! wgpu asks for anyway. `iris/tests/mask_sdf.rs` does exactly that. //! //! `VK_LOADER_DISABLE_DYNAMIC_LIBRARY_UNLOADING=1` also makes every mode //! exit cleanly, which is the confirmation that the unload is the //! mechanism -- but it is an environment variable every caller would have //! to remember, so it belongs in this comment rather than in a script. use ash::vk; use pollster::block_on; use wgpu::*; fn main() { let mode = std::env::args().nth(1).unwrap_or_else(|| "thread".into()); let body = match mode.as_str() { "main" => return wgpu_open_and_close(false), "thread" => || wgpu_open_and_close(false), "keep" => || wgpu_open_and_close(true), "raw" => raw_vulkan_open_and_close, other => panic!("unknown mode {other:?}: main | thread | keep | raw"), }; std::thread::spawn(body).join().expect("the spawned thread"); // Not reached when the thread's exit takes the process with it. eprintln!("thread joined"); } /// A wgpu instance and device, opened and closed. `keep_instance` is the /// fix under test: everything else still drops normally. fn wgpu_open_and_close(keep_instance: bool) { let instance = Instance::default(); let adapter = block_on(instance.request_adapter(&RequestAdapterOptions::default())).expect("no adapter"); let info = adapter.get_info(); eprintln!( "adapter: {} ({:?}, {})", info.name, info.backend, info.driver ); let (device, queue) = block_on(adapter.request_device(&DeviceDescriptor::default())).expect("no device"); drop(queue); drop(device); drop(adapter); if keep_instance { std::mem::forget(instance); } else { drop(instance); } eprintln!("wgpu closed"); } /// The same shape with no wgpu in it at all, which is what makes this a /// loader/driver bug rather than a wgpu one. fn raw_vulkan_open_and_close() { unsafe { let entry = ash::Entry::load().expect("vulkan loader"); let app = vk::ApplicationInfo::default().api_version(vk::make_api_version(0, 1, 1, 0)); let instance = entry .create_instance( &vk::InstanceCreateInfo::default().application_info(&app), None, ) .expect("instance"); let phys = instance.enumerate_physical_devices().expect("devices")[0]; let priorities = [1.0f32]; let queues = [vk::DeviceQueueCreateInfo::default() .queue_family_index(0) .queue_priorities(&priorities)]; let device = instance .create_device( phys, &vk::DeviceCreateInfo::default().queue_create_infos(&queues), None, ) .expect("device"); device.destroy_device(None); instance.destroy_instance(None); } eprintln!("raw vulkan closed"); }