iris: the GPU test's crash was the Vulkan loader unloading Mesa, not wgpu
`mask_sdf` SIGSEGVd after printing `test result: ok`, and the workaround was to hand the device to the process with `mem::forget` on the reading that "dropping a wgpu device on Venus segfaults". Every part of that except the symptom was wrong. `rigs/gpu-probe`'s new `teardown` bin is the experiment, one variable per mode: the same open-and-close exits 0 on the main thread and SIGSEGVs on a spawned one; it needs no GPU work and no device, only an instance; raw `ash` does it with no wgpu involved at all; and keeping the instance alive fixes it. Destroying the last VkInstance makes the loader dlclose the ICD, and Mesa's ICD here registers a pthread_key_create destructor into its own text without `-z nodelete`, so glibc calls it through unmapped memory when the thread exits. libtest runs every #[test] on a spawned thread, which is the whole reason this looked like a drop bug. `VK_LOADER_DISABLE_DYNAMIC_LIBRARY_UNLOADING=1` confirms the mechanism. So the fix is one `wgpu::Instance` for the process -- what wgpu asks for anyway -- and the device, queue and everything else drop normally again. The escape and its paragraph of reasons are gone. Also: the machine-level graphics notes duplicated in docs/RUST.md, run-headless.sh and two source comments now point at the `this-machine-graphics` skill, which is the only copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f014e8d9cf
commit
b9924e7617
6 files changed
+229
-160
No files matched your search
@@ -0,0 +1,102 @@
|
||||
//! 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");
|
||||
}
|
||||
Reference in new issue
Block a user