render() can loop forever reconfiguring the surface when the cached surface size is stale
#460 aperta il 2 ago 2026
Metriche repository
- Star
- (2121 stelle)
- Metriche merge PR
- (Metriche PR in attesa)
Descrizione
I hit a hard hang in an app using pixels 0.17.1 on Linux/X11 and traced it to the retry loop in render_with:
let frame = loop {
match self.context.surface.get_current_texture() {
...
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.reconfigure_surface();
}
...
}
};
reconfigure_surface() reconfigures using self.surface_size, which nothing but resize_surface() ever updates. So if that cached size disagrees with the real window size, each iteration builds a swapchain at the wrong extent, Mesa's X11 WSI hands back OUT_OF_DATE for it again, and round we go. The loop has no bound and no way to change the size from the inside, so it is a livelock rather than a retry.
What makes it easy to hit is that render() normally runs inside the winit event callback. Between the WM resizing the X window and the app getting Resized and calling resize_surface, the cached size is stale, and if a redraw is dispatched before the resize event the loop starts. It never returns, so the event loop can no longer deliver the Resized event that would have fixed the size. I hit it entering/exiting fullscreen, twice, but any resize where the events land in that order should do it.
Backtrace from the hung process (release build without debug symbols, Debian testing, X11, AMD RX 9070 XT on Mesa radv):
#3 poll () from libc.so.6
#6 xcb_wait_for_reply () from libxcb.so.1
#7 xcb_generate_id () from libxcb.so.1
#8 ?? () from libvulkan_radeon.so
#11 <wgpu_hal::vulkan::swapchain::native::NativeSurface as wgpu_hal::vulkan::swapchain::Surface>::create_swapchain ()
#12 <S as wgpu_hal::dynamic::surface::DynSurface>::configure ()
#13 wgpu::api::surface::Surface::configure ()
#14 pixels::Pixels::render ()
#15 <myapp::App as winit::application::ApplicationHandler>::window_event ()
Two things in that dump show how long it had been going round:
- It is blocked in
xcb_generate_id, which only makes a round trip (XC-MISCGetXIDRange) once the client has burned through its whole XID space. That is 0x1FFFFF ids by default and a Mesa X11 swapchain costs something like ten of them, so on the order of 200k swapchains had been created and dropped. - The live
WSI swapchain q/WSI swapchain ethreads (Mesa spawns a pair per swapchain under FIFO) had LWP numbers about 431,000 above the process's own threads, which is the same ~215k swapchains counted a different way.
So it is not a deadlock as such, it just churns swapchains flat out until the X connection wedges on XID allocation. From the outside it looks like the app froze with one core pinned.
I have worked around it by re-syncing the surface to window.inner_size() before every render, which keeps the cached size honest so the retry converges. That only closes the deterministic case though: if the window gets resized between that check and the acquire, the loop is unbounded again, and an application cannot do anything about it from outside since surface_size is private and Pixels holds no window handle to query.
Would you take a patch that bounds the loop? A few attempts and then returning the surface error seems reasonable to me, so a caller gets a dropped frame instead of a hung process, and the existing behaviour from #121 and #346 is preserved for the transient case those were about.
Versions: pixels 0.17.1 and 0.17.2, wgpu 29, winit 0.30.13. The loop is unchanged on main (47c09b9).