`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>
103 lines
4.1 KiB
Rust
103 lines
4.1 KiB
Rust
//! 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");
|
|
}
|