|
| 1 | +//! Repro for a binary-semaphore reuse hazard in `wgpu_hal::vulkan`'s swapchain |
| 2 | +//! acquire path. |
| 3 | +//! |
| 4 | +//! `wgpu_core::present::Surface::get_current_texture_inner` calls the real |
| 5 | +//! `vkAcquireNextImageKHR` (via `hal::Surface::acquire_texture`) *before* it |
| 6 | +//! checks whether a texture is already outstanding: |
| 7 | +//! |
| 8 | +//! ```ignore |
| 9 | +//! let (texture, status) = match unsafe { suf.acquire_texture(...) } { |
| 10 | +//! Ok(ast) => { |
| 11 | +//! // ... wraps `ast` in a `Texture` ... |
| 12 | +//! if present.acquired_texture.is_some() { |
| 13 | +//! return Err(SurfaceError::AlreadyAcquired); // too late: we already acquired! |
| 14 | +//! } |
| 15 | +//! present.acquired_texture = Some(texture.clone()); |
| 16 | +//! // ... |
| 17 | +//! } |
| 18 | +//! // ... |
| 19 | +//! }; |
| 20 | +//! ``` |
| 21 | +//! |
| 22 | +//! So calling [`wgpu::Surface::get_current_texture`] a second time before |
| 23 | +//! presenting (or discarding) the first result doesn't harmlessly no-op: it |
| 24 | +//! performs a second, real acquisition, then throws the result away as a |
| 25 | +//! validation error. That thrown-away acquisition's `VkSemaphore` was |
| 26 | +//! signaled by the presentation engine, but — because it's discarded before |
| 27 | +//! ever becoming a [`wgpu::SurfaceTexture`] — it never goes through |
| 28 | +//! `wgpu::SurfaceTexture`'s `Drop` (which would call `Surface::discard`, and |
| 29 | +//! properly release the semaphore). It also never gets used in a |
| 30 | +//! `Queue::submit` call, which is the *only* place `wgpu_hal::vulkan` ever |
| 31 | +//! waits on one of these semaphores |
| 32 | +//! (see `wgpu_hal::vulkan::swapchain::native::SwapchainAcquireSemaphore`). |
| 33 | +//! |
| 34 | +//! Each acquisition — good or thrown-away — advances the swapchain's |
| 35 | +//! acquire-semaphore ring irrespective of whether that semaphore ever gets |
| 36 | +//! waited on. So a semaphore poisoned this way eventually comes back around |
| 37 | +//! and gets passed to `vkAcquireNextImageKHR` a second time while still |
| 38 | +//! signaled from the first, unwaited acquisition — violating |
| 39 | +//! [VUID-vkAcquireNextImageKHR-semaphore-01286][vuid], which requires the |
| 40 | +//! semaphore to be unsignaled. |
| 41 | +//! |
| 42 | +//! [vuid]: https://docs.vulkan.org/spec/latest/chapters/VK_KHR_surface/wsi.html#VUID-vkAcquireNextImageKHR-semaphore-01286 |
| 43 | +//! |
| 44 | +//! Run with `RUST_LOG=wgpu_hal=info` (or similar) to see the Vulkan |
| 45 | +//! validation layer's report. Note that `wgpu_hal::vulkan`'s swapchain also |
| 46 | +//! has a *separate*, already-identified bug where the per-swapchain fence |
| 47 | +//! passed to every `vkAcquireNextImageKHR` call is never waited on or reset |
| 48 | +//! outside Windows, which trips VUID-vkAcquireNextImageKHR-fence-10066 on |
| 49 | +//! the very first repeated acquire. That VUID — and, once enough images have |
| 50 | +//! been leaked, other synchronization-hazard warnings too — will almost |
| 51 | +//! certainly show up in the log as noise. This example is specifically about |
| 52 | +//! the semaphore one, VUID-vkAcquireNextImageKHR-semaphore-01286. |
| 53 | +
|
| 54 | +use std::sync::Arc; |
| 55 | + |
| 56 | +use winit::application::ApplicationHandler; |
| 57 | +use winit::event::WindowEvent; |
| 58 | +use winit::event_loop::{ActiveEventLoop, EventLoop}; |
| 59 | +use winit::window::{Window, WindowId}; |
| 60 | + |
| 61 | +/// How many acquire/present cycles to run. Each cycle poisons one semaphore |
| 62 | +/// slot, so this only needs to exceed the swapchain's image count (usually |
| 63 | +/// 2-4) for the ring to wrap around onto a poisoned slot. Kept small: every |
| 64 | +/// poisoned acquisition also permanently holds a real swapchain image (it's |
| 65 | +/// thrown away before we ever get a handle to present or discard it), so |
| 66 | +/// enough iterations will exhaust the image pool and turn later acquisitions |
| 67 | +/// into genuine (slow) timeouts rather than new violations. |
| 68 | +const ITERATIONS: u32 = 6; |
| 69 | + |
| 70 | +fn main() { |
| 71 | + env_logger::init(); |
| 72 | + let event_loop = EventLoop::new().unwrap(); |
| 73 | + event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); |
| 74 | + let mut app = App::default(); |
| 75 | + event_loop.run_app(&mut app).unwrap(); |
| 76 | +} |
| 77 | + |
| 78 | +#[derive(Default)] |
| 79 | +struct App { |
| 80 | + state: Option<State>, |
| 81 | + ran: bool, |
| 82 | +} |
| 83 | + |
| 84 | +struct State { |
| 85 | + window: Arc<Window>, |
| 86 | + device: wgpu::Device, |
| 87 | + queue: wgpu::Queue, |
| 88 | + surface: wgpu::Surface<'static>, |
| 89 | +} |
| 90 | + |
| 91 | +impl ApplicationHandler for App { |
| 92 | + fn resumed(&mut self, event_loop: &ActiveEventLoop) { |
| 93 | + if self.state.is_some() { |
| 94 | + return; |
| 95 | + } |
| 96 | + let window = Arc::new( |
| 97 | + event_loop |
| 98 | + .create_window(Window::default_attributes().with_title("Semaphore reuse repro")) |
| 99 | + .unwrap(), |
| 100 | + ); |
| 101 | + self.state = Some(State::new(window)); |
| 102 | + } |
| 103 | + |
| 104 | + fn window_event( |
| 105 | + &mut self, |
| 106 | + event_loop: &ActiveEventLoop, |
| 107 | + _window_id: WindowId, |
| 108 | + event: WindowEvent, |
| 109 | + ) { |
| 110 | + let Some(state) = &mut self.state else { return }; |
| 111 | + match event { |
| 112 | + WindowEvent::CloseRequested => event_loop.exit(), |
| 113 | + WindowEvent::RedrawRequested => { |
| 114 | + // Run once: this is a one-shot repro, not an animation. |
| 115 | + if !self.ran { |
| 116 | + self.ran = true; |
| 117 | + state.run_repro(); |
| 118 | + event_loop.exit(); |
| 119 | + } |
| 120 | + } |
| 121 | + _ => {} |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { |
| 126 | + if let Some(state) = &self.state { |
| 127 | + state.window.request_redraw(); |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +impl State { |
| 133 | + fn new(window: Arc<Window>) -> Self { |
| 134 | + let size = window.inner_size(); |
| 135 | + let width = size.width.max(1); |
| 136 | + let height = size.height.max(1); |
| 137 | + |
| 138 | + let mut instance_desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env(); |
| 139 | + // Force the Vulkan validation layer on, regardless of the caller's |
| 140 | + // environment, so the VUID report shows up unconditionally. |
| 141 | + instance_desc.flags |= wgpu::InstanceFlags::debugging(); |
| 142 | + let instance = wgpu::Instance::new(instance_desc); |
| 143 | + let surface = instance.create_surface(window.clone()).unwrap(); |
| 144 | + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { |
| 145 | + compatible_surface: Some(&surface), |
| 146 | + ..Default::default() |
| 147 | + })) |
| 148 | + .expect("No adapter"); |
| 149 | + |
| 150 | + println!( |
| 151 | + "Adapter: {:?} ({:?})", |
| 152 | + adapter.get_info().name, |
| 153 | + adapter.get_info().backend |
| 154 | + ); |
| 155 | + |
| 156 | + let (device, queue) = |
| 157 | + pollster::block_on(adapter.request_device(&Default::default())).unwrap(); |
| 158 | + |
| 159 | + let surface_format = surface.get_capabilities(&adapter).formats[0]; |
| 160 | + let surface_config = wgpu::SurfaceConfiguration { |
| 161 | + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, |
| 162 | + format: surface_format, |
| 163 | + color_space: wgpu::SurfaceColorSpace::Auto, |
| 164 | + width, |
| 165 | + height, |
| 166 | + present_mode: wgpu::PresentMode::AutoVsync, |
| 167 | + alpha_mode: wgpu::CompositeAlphaMode::Auto, |
| 168 | + view_formats: vec![], |
| 169 | + desired_maximum_frame_latency: 2, |
| 170 | + }; |
| 171 | + surface.configure(&device, &surface_config); |
| 172 | + |
| 173 | + State { |
| 174 | + window, |
| 175 | + device, |
| 176 | + queue, |
| 177 | + surface, |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + /// Repeatedly acquire a texture, then — before presenting it — acquire a |
| 182 | + /// *second* one. The second acquisition is real at the Vulkan level (see |
| 183 | + /// the module doc comment) but gets reported to us as a validation |
| 184 | + /// error, and its handle is thrown away. Only then do we present the |
| 185 | + /// first, legitimately-held texture and move to the next iteration. |
| 186 | + fn run_repro(&mut self) { |
| 187 | + println!( |
| 188 | + "Running {ITERATIONS} acquire/poison/present cycles; watch the log for \ |
| 189 | + VUID-vkAcquireNextImageKHR-semaphore-01286." |
| 190 | + ); |
| 191 | + for i in 0..ITERATIONS { |
| 192 | + let held = match self.surface.get_current_texture() { |
| 193 | + wgpu::CurrentSurfaceTexture::Success(t) |
| 194 | + | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t, |
| 195 | + other => { |
| 196 | + println!("[{i}] first get_current_texture() returned {other:?}, aborting"); |
| 197 | + return; |
| 198 | + } |
| 199 | + }; |
| 200 | + |
| 201 | + // Deliberately violate the "present or drop before re-acquiring" |
| 202 | + // contract. Wrap it in an error scope so the validation error |
| 203 | + // `get_current_texture` raises for `AlreadyAcquired` doesn't |
| 204 | + // panic (there's no `on_uncaptured_error` handler registered). |
| 205 | + let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation); |
| 206 | + let poisoned = self.surface.get_current_texture(); |
| 207 | + let error = pollster::block_on(scope.pop()); |
| 208 | + match (&poisoned, &error) { |
| 209 | + (wgpu::CurrentSurfaceTexture::Validation, Some(err)) => { |
| 210 | + println!("[{i}] second get_current_texture() raised (expected): {err}"); |
| 211 | + } |
| 212 | + (wgpu::CurrentSurfaceTexture::Timeout, None) => { |
| 213 | + println!( |
| 214 | + "[{i}] second get_current_texture() timed out — likely because earlier \ |
| 215 | + leaked acquisitions have exhausted the swapchain's image pool (expected \ |
| 216 | + once enough images have been leaked; not a sign the bug is fixed)" |
| 217 | + ); |
| 218 | + } |
| 219 | + _ => { |
| 220 | + println!( |
| 221 | + "[{i}] second get_current_texture() returned {poisoned:?} / {error:?} \ |
| 222 | + (expected a Validation status with an AlreadyAcquired error — did the \ |
| 223 | + AlreadyAcquired check move earlier than the acquire call?)" |
| 224 | + ); |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + // Present the texture we legitimately hold. We never wrote to |
| 229 | + // it, so wgpu inserts an implicit clear-and-transition |
| 230 | + // submission, which correctly waits on and consumes *its* |
| 231 | + // acquire semaphore. The second, thrown-away acquisition's |
| 232 | + // semaphore gets no such treatment. |
| 233 | + self.queue.present(held); |
| 234 | + } |
| 235 | + println!( |
| 236 | + "Done. If a poisoned semaphore slot got reacquired above, the Vulkan \ |
| 237 | + validation layer should have logged VUID-vkAcquireNextImageKHR-semaphore-01286." |
| 238 | + ); |
| 239 | + } |
| 240 | +} |
0 commit comments