Skip to content

Commit

Permalink
[hal/vk] Rework Submission and Surface Synchronization (gfx-rs#5681)
Browse files Browse the repository at this point in the history
Fix two major synchronization issues in `wgpu_val::vulkan`:

- Properly order queue command buffer submissions. Due to Mesa bugs, two semaphores are required even though the Vulkan spec says that only one should be necessary.

- Properly manage surface texture acquisition and presentation:

    - Acquiring a surface texture can return while the presentation engine is still displaying the texture. Applications must wait for a semaphore to be signaled before using the acquired texture.

    - Presenting a surface texture requires a semaphore to ensure that drawing is complete before presentation occurs.

Co-authored-by: Jim Blandy <[email protected]>
  • Loading branch information
2 people authored and morr0ne committed Jul 28, 2024
1 parent 7128f7c commit 7893c21
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 13 deletions.
32 changes: 31 additions & 1 deletion wgpu-core/src/device/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,37 @@ impl Global {
}
}

if let Some(pending_execution) = pending_writes.pre_submit(
let refs = pending_writes
.pre_submit()?
.into_iter()
.chain(
active_executions
.iter()
.flat_map(|pool_execution| pool_execution.cmd_buffers.iter()),
)
.collect::<Vec<_>>();

let mut submit_surface_textures =
SmallVec::<[_; 2]>::with_capacity(submit_surface_textures_owned.len());

for texture in &submit_surface_textures_owned {
submit_surface_textures.extend(match texture.inner.get(&snatch_guard) {
Some(TextureInner::Surface { raw, .. }) => raw.as_ref(),
_ => None,
});
}

unsafe {
queue
.raw
.as_ref()
.unwrap()
.submit(&refs, &submit_surface_textures, (fence, submit_index))
.map_err(DeviceError::from)?;
}

profiling::scope!("cleanup");
if let Some(pending_execution) = pending_writes.post_submit(
&device.command_allocator,
device.raw(),
queue.raw.as_ref().unwrap(),
Expand Down
3 changes: 0 additions & 3 deletions wgpu-hal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,9 +956,6 @@ pub trait Queue: WasmNotSendSync {
/// - All calls to this function that include a given [`SurfaceTexture`][st]
/// in `surface_textures` must use the same [`Fence`].
///
/// - The [`Fence`] passed as `signal_fence.0` must remain alive until
/// all submissions that will signal it have completed.
///
/// [`Fence`]: Api::Fence
/// [cb]: Api::CommandBuffer
/// [ce]: Api::CommandEncoder
Expand Down
67 changes: 67 additions & 0 deletions wgpu-hal/src/vulkan/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2459,6 +2459,73 @@ impl super::DeviceShared {
}
}

impl super::DeviceShared {
pub(super) fn new_binary_semaphore(&self) -> Result<vk::Semaphore, crate::DeviceError> {
unsafe {
self.raw
.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
.map_err(crate::DeviceError::from)
}
}

pub(super) fn wait_for_fence(
&self,
fence: &super::Fence,
wait_value: crate::FenceValue,
timeout_ns: u64,
) -> Result<bool, crate::DeviceError> {
profiling::scope!("Device::wait");
match *fence {
super::Fence::TimelineSemaphore(raw) => {
let semaphores = [raw];
let values = [wait_value];
let vk_info = vk::SemaphoreWaitInfo::builder()
.semaphores(&semaphores)
.values(&values);
let result = match self.extension_fns.timeline_semaphore {
Some(super::ExtensionFn::Extension(ref ext)) => unsafe {
ext.wait_semaphores(&vk_info, timeout_ns)
},
Some(super::ExtensionFn::Promoted) => unsafe {
self.raw.wait_semaphores(&vk_info, timeout_ns)
},
None => unreachable!(),
};
match result {
Ok(()) => Ok(true),
Err(vk::Result::TIMEOUT) => Ok(false),
Err(other) => Err(other.into()),
}
}
super::Fence::FencePool {
last_completed,
ref active,
free: _,
} => {
if wait_value <= last_completed {
Ok(true)
} else {
match active.iter().find(|&&(value, _)| value >= wait_value) {
Some(&(_, raw)) => {
match unsafe {
self.raw.wait_for_fences(&[raw], true, timeout_ns)
} {
Ok(()) => Ok(true),
Err(vk::Result::TIMEOUT) => Ok(false),
Err(other) => Err(other.into()),
}
}
None => {
log::error!("No signals reached value {}", wait_value);
Err(crate::DeviceError::Lost)
}
}
}
}
}
}
}

impl From<gpu_alloc::AllocationError> for crate::DeviceError {
fn from(error: gpu_alloc::AllocationError) -> Self {
use gpu_alloc::AllocationError as Ae;
Expand Down
86 changes: 77 additions & 9 deletions wgpu-hal/src/vulkan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,7 @@ mod conv;
mod device;
mod instance;

use std::{
borrow::Borrow,
collections::HashSet,
ffi::{CStr, CString},
fmt, mem,
num::NonZeroU32,
sync::Arc,
};
use std::{borrow::Borrow, collections::HashSet, ffi::CStr, fmt, mem, num::NonZeroU32, sync::Arc};

use arrayvec::ArrayVec;
use ash::{ext, khr, vk};
Expand Down Expand Up @@ -618,6 +611,81 @@ impl RelaySemaphores {
}
}

/// Semaphores for forcing queue submissions to run in order.
///
/// The [`wgpu_hal::Queue`] trait promises that if two calls to [`submit`] are
/// ordered, then the first submission will finish on the GPU before the second
/// submission begins. To get this behavior on Vulkan we need to pass semaphores
/// to [`vkQueueSubmit`] for the commands to wait on before beginning execution,
/// and to signal when their execution is done.
///
/// Normally this can be done with a single semaphore, waited on and then
/// signalled for each submission. At any given time there's exactly one
/// submission that would signal the semaphore, and exactly one waiting on it,
/// as Vulkan requires.
///
/// However, as of Oct 2021, bug [#5508] in the Mesa ANV drivers caused them to
/// hang if we use a single semaphore. The workaround is to alternate between
/// two semaphores. The bug has been fixed in Mesa, but we should probably keep
/// the workaround until, say, Oct 2026.
///
/// [`wgpu_hal::Queue`]: crate::Queue
/// [`submit`]: crate::Queue::submit
/// [`vkQueueSubmit`]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#vkQueueSubmit
/// [#5508]: https://gitlab.freedesktop.org/mesa/mesa/-/issues/5508
#[derive(Clone)]
struct RelaySemaphores {
/// The semaphore the next submission should wait on before beginning
/// execution on the GPU. This is `None` for the first submission, which
/// should not wait on anything at all.
wait: Option<vk::Semaphore>,

/// The semaphore the next submission should signal when it has finished
/// execution on the GPU.
signal: vk::Semaphore,
}

impl RelaySemaphores {
fn new(device: &DeviceShared) -> Result<Self, crate::DeviceError> {
Ok(Self {
wait: None,
signal: device.new_binary_semaphore()?,
})
}

/// Advances the semaphores, returning the semaphores that should be used for a submission.
fn advance(&mut self, device: &DeviceShared) -> Result<Self, crate::DeviceError> {
let old = self.clone();

// Build the state for the next submission.
match self.wait {
None => {
// The `old` values describe the first submission to this queue.
// The second submission should wait on `old.signal`, and then
// signal a new semaphore which we'll create now.
self.wait = Some(old.signal);
self.signal = device.new_binary_semaphore()?;
}
Some(ref mut wait) => {
// What this submission signals, the next should wait.
mem::swap(wait, &mut self.signal);
}
};

Ok(old)
}

/// Destroys the semaphores.
unsafe fn destroy(&self, device: &ash::Device) {
unsafe {
if let Some(wait) = self.wait {
device.destroy_semaphore(wait, None);
}
device.destroy_semaphore(self.signal, None);
}
}
}

pub struct Queue {
raw: vk::Queue,
swapchain_fn: khr::swapchain::Device,
Expand Down Expand Up @@ -1081,7 +1149,7 @@ impl crate::Queue for Queue {

let swapchains = [ssc.raw];
let image_indices = [texture.index];
let vk_info = vk::PresentInfoKHR::default()
let vk_info = vk::PresentInfoKHR::builder()
.swapchains(&swapchains)
.image_indices(&image_indices)
.wait_semaphores(swapchain_semaphores.get_present_wait_semaphores());
Expand Down

0 comments on commit 7893c21

Please sign in to comment.