qt/wayland: stretch subsurface buffer on resize via wp_viewport

Eliminates the resize bleed where the parent QWidget had grown to
its new size but our wl_subsurface still had the old-size buffer
attached, briefly exposing the translucent parent (= compositor
background) in the new area.

Trick: wp_viewport.set_destination is double-buffered on the
child surface. Committing the child with a new destination but
WITHOUT a new buffer makes the compositor stretch the currently-
attached buffer to fill the new extent. We do this at the start
of syncSurfaceSize, before the synchronous ghostty_surface_draw
that renders the proper new-size frame. Sequence per resize:

  1. wp_viewport.set_destination(new_w_logical, new_h_logical)
     + wl_surface.commit(child)  -> stretch old buffer immediately
  2. ghostty_surface_draw + drainVulkan  -> attach + commit
     proper new-size buffer, replacing the stretched content
  3. Qt commits parent surface at new size  -> parent grows with
     subsurface already filled (step 1) or already correct (step 2)

The only artifact is one frame of brief stretching of old content
instead of a one-frame transparent gap — visually similar to mpv's
vo_dmabuf_wayland behavior on video window resize.

Same idea any subsurface-based compositor client uses; no Wayland
protocol provides true atomic-commit-N-surfaces (researched), so
this visual hide is the universal solution.

Stays in desync mode — sync mode requires the parent to commit
for cached child state to apply, which fails for a translucent
QWidget that has no paint damage.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-25 10:46:02 -05:00
parent 8f2bb90ec5
commit 0b32bebaeb
3 changed files with 47 additions and 1 deletions

View File

@ -247,8 +247,18 @@ void GhosttySurface::syncSurfaceSize() {
// before the subsurface present path replaced the QImage one.
if (m_useSubsurface.load(std::memory_order_acquire) &&
m_subsurfacePresenter) {
// First: stretch the existing subsurface buffer to the new
// logical size by bumping wp_viewport.set_destination + a bare
// child commit. In desync mode the compositor applies this
// immediately, so the parent surface can grow to the new size
// with our subsurface already covering it (briefly stretched)
// instead of exposing a transparent gap. mpv's
// vo_dmabuf_wayland uses the same pattern for video resize.
m_subsurfacePresenter->resizeDestination(width(), height());
// Then: render at the new size and commit the proper new-size
// buffer, which overwrites the stretched content.
ghostty_surface_draw(m_surface);
drainVulkan(); // runs presentDmabuf at the new size + commits
drainVulkan();
return;
}

View File

@ -428,4 +428,22 @@ void SubsurfacePresenter::presentDmabuf(int fd, uint32_t drm_format,
}
}
void SubsurfacePresenter::resizeDestination(int dest_width, int dest_height) {
if (!m_viewport || !m_childSurface) return;
if (dest_width <= 0 || dest_height <= 0) return;
if (dest_width == m_lastDestWidth && dest_height == m_lastDestHeight) return;
// Update destination + commit child WITHOUT attaching a new buffer.
// In desync mode the commit applies immediately and the compositor
// stretches the currently-attached buffer to the new dest extent.
// The next presentDmabuf will overwrite this with a properly-sized
// buffer, but until then the subsurface fills the new area instead
// of leaving a transparent gap during the parent's resize commit.
wp_viewport_set_destination(m_viewport, dest_width, dest_height);
m_lastDestWidth = dest_width;
m_lastDestHeight = dest_height;
wl_surface_commit(m_childSurface);
wl_display_flush(m_display);
}
} // namespace wayland

View File

@ -102,6 +102,24 @@ public:
// `logical * preferredScale120() / 120` device pixels.
uint32_t preferredScale120() const { return m_preferredScale120; }
// Stretch the existing subsurface buffer to a new destination
// size WITHOUT attaching a new buffer. Used at the *start* of a
// resize, before the renderer has produced a new-size frame:
// wp_viewport.set_destination is double-buffered on the child
// surface, so committing the child here in desync mode applies
// the new destination immediately and the compositor stretches
// the old buffer to fill it. Result: the parent surface can grow
// to its new size with the subsurface already covering the new
// area (briefly stretched), instead of leaving a one-frame
// transparent gap where the translucent parent shows through.
//
// The next presentDmabuf call (with the real new-size buffer)
// replaces the stretched content, ending the brief blur.
//
// Same pattern mpv's vo_dmabuf_wayland uses for its video
// subsurface during resize.
void resizeDestination(int dest_width, int dest_height);
// Called from the wp_fractional_scale_v1.preferred_scale event.
// Public so the C-style listener struct at file scope in the .cpp
// can name it; not part of the API for other call sites.