Skip to content

Commit 9312456

Browse files
committed
Add examples/bug-repro/03_semaphore_reuse, to demonstrate a bug.
If you run this example on the Vulkan backend with the validation layers enabled, this demonstrates a violation of VUID-vkAcquireNextImageKHR-semaphore-01286. Comments in the code explain what's going on.
1 parent 4e58833 commit 9312456

3 files changed

Lines changed: 261 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "wgpu-bug-repro-03-semaphore-reuse"
3+
edition = "2021"
4+
rust-version = "1.87"
5+
publish = false
6+
7+
[dependencies]
8+
env_logger.workspace = true
9+
pollster.workspace = true
10+
wgpu.workspace = true
11+
winit.workspace = true
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

Comments
 (0)