qt/vulkan: synchronous draw inside resizeEvent

`syncSurfaceSize` used to set the size, mark the surface dirty,
and return — leaving the next paintEvent (which Qt fires
immediately after resizeEvent) to blit the OLD frame at the OLD
size in the top-left corner while the parent window background
showed through everywhere the widget had just grown. OpenGL
didn't have this because it renders synchronously inside
`renderTerminal()`.

Drive the same model on Vulkan: after `ghostty_surface_set_size`,
call `ghostty_surface_draw` (which `Surface.draw` documents as
safe from the main thread) and drain `m_pending → m_image`
in-place before returning. The paintEvent that immediately
follows now finds the new-size frame already in `m_image`.

Still gated on `!m_image.isNull()` — calling `ghostty_surface_draw`
before the first frame deadlocks against Qt first-show event
delivery during Vulkan host bring-up.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 21:10:34 -05:00
parent 1427f658aa
commit 6ba3d06b92
1 changed files with 31 additions and 12 deletions

View File

@ -216,23 +216,42 @@ void GhosttySurface::syncSurfaceSize() {
m_fbDpr = dpr;
// Vulkan path: libghostty manages the target image itself (it
// allocates the dmabuf-exportable VkImage). We just need to tell
// it the new pixel size + DPR — the renderer thread picks up
// the new size and produces frames on its own clock; the
// GUI-thread polling timer (`m_vulkanPollTimer`) picks them up.
// allocates the dmabuf-exportable VkImage). Tell it the new
// pixel size + DPR, then drive a synchronous draw at the new
// size so the QPaintEvent Qt will deliver right after this
// resizeEvent returns paints the new geometry — not the previous
// frame in the previous-size corner with the surrounding area
// showing the parent window background.
//
// We deliberately do NOT call `renderTerminal()` synchronously
// from inside `resizeEvent`: that was deadlocking with Qt's
// first-show event delivery during bring-up. Instead we mark the
// surface dirty so the next 60Hz frame-timer tick triggers a
// render at the new size. Without this, a resize would only
// re-render if something else (PTY output, cursor blink, etc.)
// happened to flag the surface dirty later, which can leave the
// old frame stretched across the new widget for a long time.
// First-frame caveat: `ghostty_surface_draw` deadlocked during
// bring-up when called before the renderer thread had emitted
// anything (first-show races a not-yet-ready Vulkan host setup).
// Gate the synchronous draw on already having a frame —
// `m_image.isNull()` is true exclusively until the first frame
// imports. Before then we keep the original "mark dirty + let
// the timer pick it up" path.
if (m_useVulkan) {
ghostty_surface_set_content_scale(m_surface, dpr, dpr);
ghostty_surface_set_size(m_surface, static_cast<uint32_t>(w),
static_cast<uint32_t>(h));
if (!m_image.isNull()) {
// Block until the renderer thread (or this thread, since
// `Surface.draw` says renderers must support being called
// from main) finishes a frame at the new size. The frame
// lands in `m_pending` via `presentVulkanDmabuf` on whichever
// thread runs the present; drain it into `m_image` here so
// we don't have to wait for the next 60Hz timer tick before
// the resized frame is visible.
ghostty_surface_draw(m_surface);
QImage frame;
{
QMutexLocker lock(&m_pendingMutex);
frame = std::move(m_pending);
}
if (!frame.isNull()) m_image = std::move(frame);
update();
return;
}
markDirty();
return;
}