Skip to content

Rework presentation#9222

Open
inner-daemons wants to merge 1 commit intogfx-rs:trunkfrom
inner-daemons:various-present-fixes
Open

Rework presentation#9222
inner-daemons wants to merge 1 commit intogfx-rs:trunkfrom
inner-daemons:various-present-fixes

Conversation

@inner-daemons
Copy link
Copy Markdown
Collaborator

@inner-daemons inner-daemons commented Mar 14, 2026

Connections
Closes #6748
Closes #9109

CC @teoxoy

Description
One question: I think surface texture acquisition should also be a queue operation since it may end up waiting for previous presents/whatever. Do you agree?

This was primarily written by Claude, though I looked over everything, tested it, and I generally guided Claude throughout this knowing the issues myself.

  • Presentation is now a queue operation (as it should've been), and keeps the surface and stuff alive as a result (also makes multi queue easier)
  • Hal queue now has a wait for complete. This is the only way in standard vulkan to wait for a present to finish
  • Present is now a queue operation. This fixes an issue with presenting when the queue has been dropped
  • Queue destruction now waits for presents to finish so that surface textures don't have use after free/UB
  • Presentation now clears the surface texture and transitions it into present mode if it wasn't used in any command buffers, fixing a bug there (present when not in present layout) and UB there (present an uninitialized texture with garbage data)

Testing
Manual test

Squash or Rebase?
Squash

Checklist

  • Run cargo fmt.
  • Run taplo format.
  • Run cargo clippy --tests. If applicable, add:
    • --target wasm32-unknown-unknown
  • Run cargo xtask test to run tests.
  • If this contains user-facing changes, add a CHANGELOG.md entry.

@atlv24
Copy link
Copy Markdown
Collaborator

atlv24 commented Mar 15, 2026

I tested this and it works, and gave it a quick look over and it seems reasonable.


// Wait for all GPU work — submissions *and* any pending presentations —
// to complete before releasing resources.
match unsafe { self.raw.wait_for_idle() } {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timeout here may be a problem in some cases?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could hang if the driver/platform doesn't itself have a timeout that causes device loss. I think this should be ok? cc @cwfitzgerald

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice that the new wait_for_idle works the same across all backends; partially resolving #6538.

@inner-daemons inner-daemons marked this pull request as ready for review March 15, 2026 17:12
@inner-daemons
Copy link
Copy Markdown
Collaborator Author

@cwfitzgerald Can you have copilot review this

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks surface presentation so that presenting is a queue operation (instead of being driven by SurfaceTexture/surface detail), adds backend support for blocking until presents complete, and fixes Vulkan correctness/UB around presenting unused/uninitialized surface textures and dropping queues with in-flight presents.

Changes:

  • Move presentation to Queue::present, removing SurfaceTexture::present and backend SurfaceOutputDetailInterface::present.
  • Add hal::Queue::wait_for_idle across backends and use it to safely wait for present completion (including on queue drop).
  • Add core-side logic to clear/transition acquired-but-unused surface textures before present, and track present lifetimes until completion.

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
wgpu/src/dispatch.rs Adds QueueInterface::present and removes surface-output-detail present hook.
wgpu/src/backend/wgpu_core.rs Routes presentation through CoreQueue::present; removes now-unused error sink from surface output detail.
wgpu/src/backend/webgpu.rs Implements no-op QueueInterface::present for web; removes output-detail present hook.
wgpu/src/api/surface_texture.rs Removes SurfaceTexture::present API; relies on Drop discard when not presented.
wgpu/src/api/surface.rs Updates presentation docs to reference Queue::present.
wgpu/src/api/queue.rs Introduces Queue::present(SurfaceTexture) -> SubmissionIndex API.
wgpu-hal/src/lib.rs Adds Queue::wait_for_idle to hal trait to support waiting for presents.
wgpu-hal/src/dynamic/queue.rs Plumbs wait_for_idle through the dynamic queue abstraction.
wgpu-hal/src/vulkan/mod.rs Implements wait_for_idle via vkQueueWaitIdle.
wgpu-hal/src/metal/mod.rs Implements wait_for_idle by committing an empty command buffer and waiting for completion.
wgpu-hal/src/dx12/mod.rs Implements wait_for_idle with a fence signal + wait.
wgpu-hal/src/gles/queue.rs Implements wait_for_idle via gl.finish().
wgpu-hal/src/noop/mod.rs Implements wait_for_idle as a no-op.
wgpu-core/src/present.rs Moves present logic onto Queue, returns a SubmissionIndex, and tracks presented textures for safe lifetime.
wgpu-core/src/device/queue.rs Adds waiting-for-present logic, queue-drop idle wait, and “unused surface texture” prepare/mini-submit path.
wgpu-core/src/device/life.rs Tracks pending present textures and releases them once completion conditions are met.
wgpu-core/src/device/resource.rs Adds maintain-time handling intended to resolve pending presents on waits.
wgpu-core/src/device/trace.rs Extends trace Action::Present to include a submission index.
wgpu-core/src/device/trace/record.rs Updates trace record conversion for the new Present action shape.
player/src/lib.rs Updates pattern match for the new traced Present action signature.
player/src/bin/play.rs Updates replay logic to present via queue/surface and handle new trace action signature.
examples/standalone/custom_backend/src/custom.rs Extends custom backend queue trait implementation with present.
examples/standalone/02_hello_window/src/main.rs Migrates example to queue.present(surface_texture).
examples/features/src/framework.rs Migrates example framework to queue.present(frame).
examples/features/src/hello_triangle/mod.rs Migrates example to queue.present(frame).
examples/features/src/hello_windows/mod.rs Migrates example to queue.present(frame).
examples/features/src/uniform_values/mod.rs Migrates example to queue.present(frame).
examples/bug-repro/01_texture_atomic_bug/src/main.rs Migrates repro to queue.present(frame).
examples/bug-repro/02_present_bugs/src/main.rs Adds new repro for presenting unused textures and dropping queue after present.
examples/bug-repro/02_present_bugs/Cargo.toml Adds new repro crate manifest.
Cargo.lock Adds lockfile entries for the new repro crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@cwfitzgerald
Copy link
Copy Markdown
Member

@teoxoy I assigned this to you, if you can't take it, feel free to unassign yourself.

@inner-daemons
Copy link
Copy Markdown
Collaborator Author

inner-daemons commented Mar 20, 2026

@teoxoy I just sent this to a matrix account but you have 2 and I'm not sure if I sent it to the right one, plsu thats probably not the right place to discuss.

Anyway, what do you think about making Surface::get_current_texture be a queue operation instead? I think it can potentially be blocking and depends on the queue timeline (a semaphore). Also we should perhaps consider adding a method to wait for the surface texture to be ready/query if its ready, or even make it an async operation.

P.S. the same probably goes for Surface::configure

@teoxoy
Copy link
Copy Markdown
Member

teoxoy commented Mar 21, 2026

IIRC configure takes a device, maybe it should take a queue instead? Is that really needed though? I guess get_current_texture can get blocked by presentation but I don't think it should be moved on the queue since it's not a queue operation conceptually.

@inner-daemons
Copy link
Copy Markdown
Collaborator Author

@teoxoy Ok, I've undone the changes that move configuration and surface texture acquisition to the queue. Should be good to review.

@inner-daemons inner-daemons requested a review from teoxoy March 24, 2026 00:46
@inner-daemons inner-daemons force-pushed the various-present-fixes branch from 77d04a7 to a695c5a Compare March 24, 2026 01:29
Copy link
Copy Markdown
Member

@teoxoy teoxoy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good overall; my biggest concern is using submission indices for presentation when they have different invariants.

Comment on lines +236 to +263
device
.last_successful_submission_index
.store(present_index, Ordering::Release);
self.lock_life().track_present(present_index, texture);
Ok((Status::Timeout, present_index))
}
hal::SurfaceError::Occluded => {
device
.last_successful_submission_index
.store(present_index, Ordering::Release);
self.lock_life().track_present(present_index, texture);
Ok((Status::Occluded, present_index))
}
hal::SurfaceError::Lost => {
device
.last_successful_submission_index
.store(present_index, Ordering::Release);
self.lock_life().track_present(present_index, texture);
Ok((Status::Lost, present_index))
}
hal::SurfaceError::Device(err) => {
Err(SurfaceError::from(device.handle_hal_error(err)))
}
hal::SurfaceError::Outdated => {
device
.last_successful_submission_index
.store(present_index, Ordering::Release);
self.lock_life().track_present(present_index, texture);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we got an error back I don't think it's correct that we are storing the present index.

result
}

/// TODO: is this needed by deno?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this function since we moved it onto the queue now.

Comment on lines +475 to +492
let mut present_lock = surface.presentation.lock();

if let Some(ref presentation) = *present_lock {
let same_device = Arc::ptr_eq(&presentation.device, &queue.device);

if !same_device {
return Err(SurfaceError::Device(DeviceError::DeviceMismatch(Box::new(
DeviceMismatch {
res: queue.error_ident(),
res_device: queue.device.error_ident(),
target: None,
target_device: presentation.device.error_ident(),
},
))));
}
}

let result = queue.present_impl(&surface, present_lock.as_mut());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the device mismatch check in present_impl and rename present_impl to present (removing the old present).


let wait_result = unsafe {
self.raw()
.wait(fence.as_ref(), target_submission_index, wait_timeout)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this hang the thread if wait_submission_index is a present index?

As far as I understand present indices are not the same as submission indices since we don't use fences for presentation. We should either see how to make presentation work with fences, use a different counter for presentation or triple check that we do the right thing if a present index is passed as a submission index in all places a submission index is currently expected.

// until the GPU finishes with it.
if let Some(encoder) = encoder_in_flight {
self.lock_life().track_submission(mini_index, vec![encoder]);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submission of PendingWrites is more complicated than this (since it's not guaranteed it will be empty before enqueueing the clear command for the surface texture), the block in this comment is needed to do it correctly: #9307 (comment). You might have to coordinate with @andyleiserson on extracting it out to be used in places like this.

Err(e) => {
panic!("Unexpected error while waiting for queue idle on drop: {e:?}");
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should still retry a few times if we get an OutOfMemory error?

unsafe fn get_timestamp_period(&self) -> f32;
/// Block until all queue operations have completed.
///
/// This is useful for waiting on presentations, which in vulkan don't have fences.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is VkSwapchainPresentFenceInfoKHR from VK_KHR_swapchain_maintenance1 but only 12% of reports support it.. That's unfortunate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it. The spec says there is an implicit semaphore operation (can't remember the exact phrasing) that vkQueueWaitIdle still waits for. But ig most drivers don't expose that :(


// Wait for all GPU work — submissions *and* any pending presentations —
// to complete before releasing resources.
match unsafe { self.raw.wait_for_idle() } {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could hang if the driver/platform doesn't itself have a timeout that causes device loss. I think this should be ok? cc @cwfitzgerald


// Wait for all GPU work — submissions *and* any pending presentations —
// to complete before releasing resources.
match unsafe { self.raw.wait_for_idle() } {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice that the new wait_for_idle works the same across all backends; partially resolving #6538.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queue should outlast device SYNC-HAZARD-WRITE-AFTER-PRESENT when presenting a surface just after getting it.

5 participants