qt/vulkan: paint a visible placeholder when no dmabuf is imported yet

The MainWindow uses `WA_TranslucentBackground` (so the terminal's
own background-opacity reaches the desktop). When the
GhosttySurface widget inside it paints nothing — which is what
happens on the Vulkan path right now because the dmabuf import +
composite isn't wired yet — the entire window becomes invisible:
fully transparent, no visible bounds, looks like the process is
running headless.

Fixes by:

  1. Tracking `m_useVulkan` on the surface so we know which path
     this widget is on.
  2. In `paintEvent`, when `m_useVulkan` is set, fill the widget
     with a muted purple (#281638) and a centered "Vulkan renderer
     / dmabuf import not yet wired" label. The QResizeOverlay still
     paints on top, so resize-grid info works.
  3. The OpenGL path is unchanged — same QImage blit as before.

While here:

  - Skip the QOpenGLContext / QOffscreenSurface / FBO setup on the
    Vulkan path. It was wasted work and may have been part of why
    the previous run silently produced no window: NVIDIA's GL+VK
    coexistence on a single Wayland surface is reportedly fragile,
    and we don't need GL at all when libghostty's renderer is
    Vulkan.

  - Drop the eager `vulkan::Host::instance()` call in `main.cpp`.
    Bringing up a VkInstance before any Qt window is mapped can
    interact poorly with Qt's Wayland integration on some
    compositors. The host is constructed lazily on the first
    GhosttySurface that needs it — same effective timing as the
    OpenGL path's context creation.

To verify the placeholder is visible:

  GHASTTY_RENDERER=vulkan ghastty-vulkan

  → muted-purple window with the placeholder text.

The OpenGL ghastty is unaffected by any of this.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 11:56:01 -05:00
parent 545898bb43
commit a473e9e2ef
3 changed files with 69 additions and 52 deletions

View File

@ -75,29 +75,41 @@ GhosttySurface::GhosttySurface(ghostty_app_t app, MainWindow *owner,
// translucent background lets that alpha reach the desktop.
setAttribute(Qt::WA_TranslucentBackground);
// A private OpenGL context for libghostty's renderer. It is never made
// current on a window — rendering goes to an offscreen framebuffer —
// so an unparented QOffscreenSurface is enough to satisfy makeCurrent.
m_context = new QOpenGLContext(this);
m_context->setFormat(QSurfaceFormat::defaultFormat());
if (!m_context->create()) {
std::fprintf(stderr, "[ghastty] GL context creation failed\n");
return;
}
m_offscreen = new QOffscreenSurface(nullptr, this);
m_offscreen->setFormat(m_context->format());
m_offscreen->create();
if (!makeCurrent()) {
std::fprintf(stderr, "[ghastty] makeCurrent failed\n");
return;
// Pick the renderer up-front so the rest of the surface setup
// (GL context vs. Vulkan host) only touches the path we'll
// actually use. Mixing the two on the same process can confuse
// some drivers (NVIDIA's GL+VK coexistence on a single Wayland
// surface is reportedly fragile); keep them disjoint.
vulkan::Host *vk_host = nullptr;
if (const char *r = std::getenv("GHASTTY_RENDERER");
r != nullptr && std::strcmp(r, "vulkan") == 0) {
vk_host = vulkan::Host::instance();
}
// A placeholder framebuffer; resizeEvent installs the real size.
QOpenGLFramebufferObjectFormat fmt;
fmt.setInternalTextureFormat(GL_RGBA8);
m_fbw = m_fbh = 16;
m_fbo = new QOpenGLFramebufferObject(QSize(m_fbw, m_fbh), fmt);
if (vk_host == nullptr) {
// OpenGL path: stand up the private context + offscreen FBO
// libghostty's GL renderer draws into.
m_context = new QOpenGLContext(this);
m_context->setFormat(QSurfaceFormat::defaultFormat());
if (!m_context->create()) {
std::fprintf(stderr, "[ghastty] GL context creation failed\n");
return;
}
m_offscreen = new QOffscreenSurface(nullptr, this);
m_offscreen->setFormat(m_context->format());
m_offscreen->create();
if (!makeCurrent()) {
std::fprintf(stderr, "[ghastty] makeCurrent failed\n");
return;
}
// A placeholder framebuffer; resizeEvent installs the real size.
QOpenGLFramebufferObjectFormat fmt;
fmt.setInternalTextureFormat(GL_RGBA8);
m_fbw = m_fbh = 16;
m_fbo = new QOpenGLFramebufferObject(QSize(m_fbw, m_fbh), fmt);
}
ghostty_surface_config_s sc =
m_parentSurface
@ -105,20 +117,8 @@ GhosttySurface::GhosttySurface(ghostty_app_t app, MainWindow *owner,
GHOSTTY_SURFACE_CONTEXT_TAB)
: ghostty_surface_config_new();
// Vulkan path: if the user opted in with `GHASTTY_RENDERER=vulkan`
// AND the process-wide Vulkan host came up at launch (see
// `main.cpp`), use the Vulkan platform plumbing. Otherwise fall
// back to the existing OpenGL path. The Vulkan-side rendering is
// still bring-up — frames are exported as dmabuf fds via the
// host's `present` callback (currently just logged); display via
// QRhiTexture import is the next chunk of Qt-side work.
vulkan::Host *vk_host = nullptr;
if (const char *r = std::getenv("GHASTTY_RENDERER");
r != nullptr && std::strcmp(r, "vulkan") == 0) {
vk_host = vulkan::Host::instance();
}
if (vk_host != nullptr) {
m_useVulkan = true;
sc.platform_tag = GHOSTTY_PLATFORM_VULKAN;
sc.platform.vulkan = vk_host->asPlatform(this);
} else {
@ -325,6 +325,23 @@ void GhosttySurface::renderTerminal() {
}
void GhosttySurface::paintEvent(QPaintEvent *) {
// Vulkan-backed surface: libghostty hands frames to the host via
// a dmabuf fd; we don't yet composite them back into this widget.
// Paint a visible placeholder so the (translucent) MainWindow
// isn't completely invisible. Replace with the imported
// QRhiTexture once the dmabuf-import path lands.
if (m_useVulkan) {
QPainter painter(this);
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.fillRect(rect(), QColor(40, 22, 56)); // muted purple — debug placeholder
painter.setPen(QColor(220, 220, 220));
painter.drawText(rect(),
Qt::AlignCenter,
QStringLiteral("Vulkan renderer\n(dmabuf import not yet wired)"));
paintResizeOverlay(painter);
return;
}
if (m_image.isNull()) return;
QPainter painter(this);
// Blit the framebuffer 1:1. m_image carries the device pixel ratio, so

View File

@ -207,12 +207,21 @@ private:
ghostty_surface_t m_parentSurface; // inherited-config source; may be null
ghostty_surface_t m_surface = nullptr;
// Private offscreen GL context libghostty renders into.
// Private offscreen GL context libghostty renders into. Null for
// the Vulkan-backed renderer (libghostty hands frames back via a
// dmabuf fd to the apprt's `present` callback — no GL involved).
QOpenGLContext *m_context = nullptr;
QOffscreenSurface *m_offscreen = nullptr;
QOpenGLFramebufferObject *m_fbo = nullptr;
QImage m_image; // last frame, read back from m_fbo
// True when this surface is using the Vulkan platform. The
// paintEvent uses this to draw a visible placeholder until the
// host-side dmabuf-import + composite work lands; otherwise the
// widget would paint nothing on a translucent window and look
// invisible.
bool m_useVulkan = false;
// GL objects for the alpha-premultiply pass.
QOpenGLShaderProgram *m_premultProg = nullptr;
QOpenGLVertexArrayObject *m_premultVao = nullptr;

View File

@ -107,23 +107,14 @@ int main(int argc, char **argv) {
return 1;
}
// GHASTTY_RENDERER=vulkan opts into the Vulkan path. When set, we
// bootstrap the process-wide Vulkan host (`vulkan::Host::instance`)
// up-front so failures (no loader, no suitable device) surface at
// launch and the user can drop the env var rather than waiting for
// the first surface to fail. The OpenGL path continues to work
// without the env var or if Vulkan bring-up fails.
if (const char *r = std::getenv("GHASTTY_RENDERER"); r != nullptr &&
std::strcmp(r, "vulkan") == 0) {
if (vulkan::Host::instance() == nullptr) {
std::fprintf(
stderr,
"[ghastty] GHASTTY_RENDERER=vulkan but Vulkan setup failed; "
"falling back to OpenGL.\n"
" Try `unset GHASTTY_RENDERER` or install vulkan-loader / "
"vulkan-headers.\n");
}
}
// The Vulkan host is intentionally NOT bootstrapped here: doing it
// before any window is mapped on Wayland can interact badly with
// Qt's Wayland integration (the VkInstance starts grabbing display
// resources before Qt has finished its own connection setup, and
// on some compositor + driver combos the result is a process that
// runs but never actually displays a window). It's brought up
// lazily on the first surface that needs it — see
// `GhosttySurface.cpp`.
// initial-window: when false, start headless (no window mapped at
// launch). Combined with quit-after-last-window-closed=false this