Environment
- wgpu-native v29.0.1.1 (binary release), wgpu-core 29.0.3
- Vulkan backend, Linux x86_64 (NVIDIA RTX 4070 Ti SUPER, XWayland xcb surface)
- Reproduced with FIFO present mode,
desired_maximum_frame_latency = 2
Summary
has_surface_presented is a single per-surface flag shared between the
surface and every WGPUTextureImpl handed out by
wgpuSurfaceGetCurrentTexture (src/lib.rs):
// wgpuSurfaceGetCurrentTexture:
surface.has_surface_presented.store(false, SeqCst); // reset on EVERY acquire
...
// wgpuSurfacePresent:
surface.has_surface_presented.store(true, SeqCst);
// impl Drop for WGPUTextureImpl:
if let Some(surface_id) = self.surface_id {
if !self.has_surface_presented.load(SeqCst) {
self.context.surface_texture_discard(surface_id) // discards the surface's CURRENT texture
}
}
surface_texture_discard operates on the surface id, i.e. on whatever
texture is currently acquired — not on the texture the dropped wrapper
represents. The flag is also reset by every acquire. So the following
perfectly legal sequence corrupts the swapchain:
wgpuSurfaceGetCurrentTexture → texture A (flag = false)
- render, submit
wgpuSurfacePresent → A presented (flag = true)
wgpuSurfaceGetCurrentTexture → texture B (flag = false again)
- the application releases its last reference to A's wrapper
(e.g. a texture view / framebuffer object of the previous frame is
destroyed after the next frame started)
WGPUTextureImpl(A)::drop sees has_surface_presented == false
→ calls surface_texture_discard(surface) → discards B
- any use of B fails:
wgpuTextureCreateView(B): "Texture with '' label has been destroyed"
- or, if the view was created before step 5,
wgpuQueueSubmit panics:
Error in wgpuQueueSubmit: Validation Error — In a pass parameter — Texture with '<Surface Texture>' label has been destroyed
(followed by fatal runtime error: failed to initiate panic / abort)
The failure is timing-dependent: if the last reference to A is dropped
between present and the next acquire (flag still true), nothing happens.
That makes the bug look like random "destroyed surface texture" crashes in
applications that keep per-frame objects (views, framebuffers) alive until
frame-completion fences fire — i.e. any engine with frames in flight.
Expected behavior
Releasing a wrapper of an already presented texture must be a no-op
(besides releasing the wgpu-core reference). Discard-on-drop should only
apply to the texture instance that was never presented — tracking must be
per-texture, not a single per-surface flag (e.g. store the acquired texture
id in the wrapper and compare before discarding, or move the flag into
WGPUTextureImpl and set it from present by identity).
Minimal reproduction (C, pseudo-code)
WGPUSurfaceTexture st1, st2;
wgpuSurfaceGetCurrentTexture(surface, &st1); // A, flag=false
/* render into A, wgpuQueueSubmit(...) */
wgpuSurfacePresent(surface); // flag=true
wgpuSurfaceGetCurrentTexture(surface, &st2); // B, flag=false (reset!)
wgpuTextureRelease(st1.texture); // drop of A discards B!
WGPUTextureView v = wgpuTextureCreateView(st2.texture, NULL);
// -> Validation Error: Texture with '<Surface Texture>' label has been destroyed
The only ordering requirement is that the release of A's wrapper happens
after the acquire of B. Holding st1.texture across the next acquire is
natural for any renderer with deferred resource destruction.
Workaround used downstream
Force-release the presented texture's wrapper (drop the C handle) right
after wgpuSurfacePresent, while has_surface_presented is still true,
and null out any cached handle so later owner destruction does not touch
wgpu. This fully restores stability.
Environment
desired_maximum_frame_latency = 2Summary
has_surface_presentedis a single per-surface flag shared between thesurface and every
WGPUTextureImplhanded out bywgpuSurfaceGetCurrentTexture(src/lib.rs):surface_texture_discardoperates on the surface id, i.e. on whatevertexture is currently acquired — not on the texture the dropped wrapper
represents. The flag is also reset by every acquire. So the following
perfectly legal sequence corrupts the swapchain:
wgpuSurfaceGetCurrentTexture→ texture A (flag = false)wgpuSurfacePresent→ A presented (flag = true)wgpuSurfaceGetCurrentTexture→ texture B (flag = false again)(e.g. a texture view / framebuffer object of the previous frame is
destroyed after the next frame started)
WGPUTextureImpl(A)::dropseeshas_surface_presented == false→ calls
surface_texture_discard(surface)→ discards BwgpuTextureCreateView(B): "Texture with '' label has been destroyed"wgpuQueueSubmitpanics:Error in wgpuQueueSubmit: Validation Error — In a pass parameter — Texture with '<Surface Texture>' label has been destroyed(followed by
fatal runtime error: failed to initiate panic/ abort)The failure is timing-dependent: if the last reference to A is dropped
between present and the next acquire (flag still true), nothing happens.
That makes the bug look like random "destroyed surface texture" crashes in
applications that keep per-frame objects (views, framebuffers) alive until
frame-completion fences fire — i.e. any engine with frames in flight.
Expected behavior
Releasing a wrapper of an already presented texture must be a no-op
(besides releasing the wgpu-core reference). Discard-on-drop should only
apply to the texture instance that was never presented — tracking must be
per-texture, not a single per-surface flag (e.g. store the acquired texture
id in the wrapper and compare before discarding, or move the flag into
WGPUTextureImpland set it from present by identity).Minimal reproduction (C, pseudo-code)
The only ordering requirement is that the release of A's wrapper happens
after the acquire of B. Holding
st1.textureacross the next acquire isnatural for any renderer with deferred resource destruction.
Workaround used downstream
Force-release the presented texture's wrapper (drop the C handle) right
after
wgpuSurfacePresent, whilehas_surface_presentedis still true,and null out any cached handle so later owner destruction does not touch
wgpu. This fully restores stability.