From 94c51e227fb3f4a99dfbd224ffe0125d0232a294 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 25 May 2026 10:29:18 -0500 Subject: [PATCH] qt/wayland: subsurface in sync mode for atomic-with-parent resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous desync mode meant our wl_subsurface commits applied immediately, independent of Qt's parent surface commits. That's lower-latency in the steady state but during resize it left a one-frame window where the parent had already grown to the new size but our child subsurface was still showing its old-size buffer — the parent's translucent QWidget background showed through the gap. The original pre-subsurface QPainter blit didn't have this issue because everything was on one surface and resize was inherently atomic. Sync mode (the wl_subsurface default) restores that atomicity: our wl_surface.commit on the child caches state until the parent commits, then both apply in the same compositor frame. Resize becomes lockstep. The cost is that frames now need a parent commit to become visible. drainVulkan now calls update() after each presentDmabuf to schedule the paintEvent that triggers Qt's backing-store flush (= parent wl_surface.commit). Latency penalty vs desync: one event-loop turn (sub-millisecond at idle). Pairs with b8d2f25cf (synchronous draw in resizeEvent) — that fix attached the new-size buffer to the subsurface inside resizeEvent; this fix ensures the attach gets applied atomically with the parent's next commit. Co-Authored-By: claude-flow --- qt/src/GhosttySurface.cpp | 6 ++++++ qt/src/wayland/SubsurfacePresenter.cpp | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index 31f17c13c..23818e50a 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -1551,6 +1551,12 @@ void GhosttySurface::drainVulkan() { m_subsurfacePresenter->presentDmabuf( frame.fd, frame.drm_format, frame.drm_modifier, frame.width, frame.height, frame.stride, width(), height()); + // The subsurface is in wl_subsurface sync mode, so the buffer + // we just attached only becomes visible when Qt's parent surface + // commits. update() schedules a paintEvent which triggers + // Qt's backing-store flush (= parent wl_surface.commit), at + // which point our cached subsurface state applies atomically. + update(); return; } diff --git a/qt/src/wayland/SubsurfacePresenter.cpp b/qt/src/wayland/SubsurfacePresenter.cpp index 6075428a8..66cf8fb20 100644 --- a/qt/src/wayland/SubsurfacePresenter.cpp +++ b/qt/src/wayland/SubsurfacePresenter.cpp @@ -254,11 +254,20 @@ SubsurfacePresenter::tryCreate(QWindow *parent) { return nullptr; } - // Independent frame pacing: the renderer's present cadence is - // driven by libghostty's render thread, not the GUI thread's paint - // cycle, so we don't want our wl_subsurface state changes to wait - // for the parent's next commit. `set_desync` is what allows that. - wl_subsurface_set_desync(sub); + // Sync mode (the wl_subsurface default — we don't call set_desync). + // In sync mode our wl_surface.commit caches state until the parent + // surface commits, at which point both apply atomically. That's + // what gives resize its lockstep behavior — parent grows to the + // new size and our subsurface's matching new-size buffer apply in + // the same compositor frame, so there's no transient gap where the + // parent's translucent background shows through. + // + // The cost: our frames need a parent commit to become visible. The + // GhosttySurface caller compensates by calling update() after each + // presentDmabuf — that schedules a paintEvent, which triggers Qt + // to flush the parent surface's backing store (= a wl_surface.commit + // on the parent). Total latency penalty vs desync: one event-loop + // turn, sub-millisecond at idle. // Subsurface covers the parent at the origin. Phase 4 will keep // this in sync on splits/tabs/etc.; for now the GhosttySurface