qt/wayland: scaffold subsurface presenter, no behavior change
New wayland::SubsurfacePresenter creates one wl_subsurface per GhosttySurface, parented to the QWindow's native wl_surface. Set desync so the renderer thread's frame cadence doesn't get held hostage by Qt's paint cycle. Position (0,0); no buffer ever attached in this commit — per protocol an uncommitted subsurface with no buffer contributes nothing to the parent's display, so the existing presentVulkanDmabuf mmap+memcpy+QPainter path keeps producing pixels exactly as before. Registry discovery follows the WindowBlur.cpp idiom: private event queue + roundtrip, then bound proxies moved back to Qt's default queue so the main dispatch drives them. Wayland-only (Qt frontend already is) — non-Wayland QPA returns nullptr from tryCreate, which Phase 2 silently tolerates and Phase 3 will treat as fatal. Lifecycle: lazy-init on first QEvent::Show when windowHandle() is non-null (sets WA_NativeWindow first); cached for the widget's lifetime so tab switches don't churn the wl_subsurface. unique_ptr destruct in ~GhosttySurface handles teardown. Verified on NVIDIA RTX 2080: "SubsurfacePresenter: subsurface ready" logs once per surface, no wl_display protocol errors, rendering identical to pre-commit. Sets up Phase 3 to wire dmabuf frames through the subsurface via zwp_linux_dmabuf_v1. Co-Authored-By: claude-flow <ruv@ruv.net>pull/12846/head
parent
07b27921d4
commit
4a890b96bd
|
|
@ -147,6 +147,7 @@ add_executable(ghastty
|
|||
src/TabWidget.cpp
|
||||
src/undo/UndoStack.cpp
|
||||
src/Util.cpp
|
||||
src/wayland/SubsurfacePresenter.cpp
|
||||
src/WindowBlur.cpp
|
||||
src/XkbTracker.cpp
|
||||
"${BLUR_CODE}"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "TabWidget.h"
|
||||
#include "Util.h"
|
||||
#include "vulkan/Host.h"
|
||||
#include "wayland/SubsurfacePresenter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
|
|
@ -306,10 +307,30 @@ bool GhosttySurface::event(QEvent *e) {
|
|||
// via parent hide / tab switch on QTabWidget. The GLArea-style
|
||||
// map/unmap signals are the same semantic.
|
||||
if (m_surface) {
|
||||
if (e->type() == QEvent::Show)
|
||||
if (e->type() == QEvent::Show) {
|
||||
ghostty_surface_set_occlusion(m_surface, true);
|
||||
else if (e->type() == QEvent::Hide)
|
||||
// First successful Show is also when our native QWindow exists
|
||||
// and we can safely look up the Wayland parent wl_surface.
|
||||
// Lazy-init the subsurface presenter once and keep it for the
|
||||
// widget's lifetime — tying it to Show/Hide would churn the
|
||||
// wl_subsurface on every tab switch.
|
||||
//
|
||||
// Phase 2 (current): scaffolding only. The presenter creates a
|
||||
// wl_subsurface but never attaches a buffer; the existing
|
||||
// `presentVulkanDmabuf` + `paintEvent` QPainter path is the
|
||||
// one producing pixels. Phase 3 will route frames through the
|
||||
// subsurface and retire the QPainter blit.
|
||||
if (!m_subsurfacePresenter) {
|
||||
// WA_NativeWindow ensures windowHandle() is non-null even if
|
||||
// GhosttySurface is embedded in a non-native parent.
|
||||
setAttribute(Qt::WA_NativeWindow);
|
||||
if (auto *h = windowHandle())
|
||||
m_subsurfacePresenter =
|
||||
wayland::SubsurfacePresenter::tryCreate(h);
|
||||
}
|
||||
} else if (e->type() == QEvent::Hide) {
|
||||
ghostty_surface_set_occlusion(m_surface, false);
|
||||
}
|
||||
}
|
||||
return QWidget::event(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <QImage>
|
||||
#include <QMutex>
|
||||
|
|
@ -12,6 +13,10 @@
|
|||
|
||||
#include "ghostty.h"
|
||||
|
||||
namespace wayland {
|
||||
class SubsurfacePresenter;
|
||||
}
|
||||
|
||||
class MainWindow;
|
||||
class QContextMenuEvent;
|
||||
class QDragEnterEvent;
|
||||
|
|
@ -307,4 +312,11 @@ private:
|
|||
// first PWD notification (libghostty fires one at spawn from the
|
||||
// inherited config, then on every cwd change).
|
||||
QString m_pwd;
|
||||
|
||||
// Wayland subsurface for the GPU-direct present path. Lazily
|
||||
// created on first `QEvent::Show` once the native QWindow exists;
|
||||
// null until then, null forever if creation fails (Phase 2 keeps
|
||||
// working in that case because nothing yet depends on it). Phase 3
|
||||
// will use this to attach dmabuf-backed `wl_buffer`s.
|
||||
std::unique_ptr<wayland::SubsurfacePresenter> m_subsurfacePresenter;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
#include "SubsurfacePresenter.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QLatin1String>
|
||||
#include <QWindow>
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
namespace wayland {
|
||||
|
||||
namespace {
|
||||
|
||||
// Process-wide bindings for the Wayland globals the presenter needs.
|
||||
// Lazily discovered on first `tryCreate`, mirrors the `blurManager`
|
||||
// pattern in `qt/src/WindowBlur.cpp` — registry roundtrip happens on
|
||||
// a private event queue so we never dispatch Qt's own Wayland events.
|
||||
struct PresenterGlobals {
|
||||
wl_compositor *compositor = nullptr;
|
||||
wl_subcompositor *subcompositor = nullptr;
|
||||
bool searched = false;
|
||||
};
|
||||
|
||||
void registryGlobal(void *data, wl_registry *registry, uint32_t name,
|
||||
const char *interface, uint32_t /*version*/) {
|
||||
auto *g = static_cast<PresenterGlobals *>(data);
|
||||
if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
|
||||
g->compositor = static_cast<wl_compositor *>(
|
||||
wl_registry_bind(registry, name, &wl_compositor_interface, 1));
|
||||
} else if (std::strcmp(interface, wl_subcompositor_interface.name) == 0) {
|
||||
g->subcompositor = static_cast<wl_subcompositor *>(
|
||||
wl_registry_bind(registry, name, &wl_subcompositor_interface, 1));
|
||||
}
|
||||
}
|
||||
void registryGlobalRemove(void *, wl_registry *, uint32_t) {}
|
||||
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
registryGlobal,
|
||||
registryGlobalRemove,
|
||||
};
|
||||
|
||||
PresenterGlobals *discoverGlobals(wl_display *display) {
|
||||
static PresenterGlobals globals;
|
||||
if (globals.searched) return &globals;
|
||||
globals.searched = true;
|
||||
|
||||
wl_event_queue *queue = wl_display_create_queue(display);
|
||||
wl_registry *registry = wl_display_get_registry(display);
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(registry), queue);
|
||||
wl_registry_add_listener(registry, &kRegistryListener, &globals);
|
||||
wl_display_roundtrip_queue(display, queue);
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
// Move the bound proxies back to the default queue so Qt's main
|
||||
// dispatch drives subsequent events on them, then drop the private
|
||||
// queue. (Same lifecycle dance as `blurManager`.)
|
||||
if (globals.compositor)
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(globals.compositor),
|
||||
nullptr);
|
||||
if (globals.subcompositor)
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(globals.subcompositor),
|
||||
nullptr);
|
||||
wl_event_queue_destroy(queue);
|
||||
|
||||
return &globals;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<SubsurfacePresenter>
|
||||
SubsurfacePresenter::tryCreate(QWindow *parent) {
|
||||
if (!parent) return nullptr;
|
||||
|
||||
// The Qt frontend is Wayland-only; if we're not on Wayland, the
|
||||
// native-interface lookups below would return null anyway, but
|
||||
// bail explicitly so the log message is useful.
|
||||
if (!QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: not on Wayland QPA\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
|
||||
if (!native) return nullptr;
|
||||
|
||||
auto *display = static_cast<wl_display *>(
|
||||
native->nativeResourceForIntegration("wl_display"));
|
||||
auto *parentSurface = static_cast<wl_surface *>(
|
||||
native->nativeResourceForWindow("surface", parent));
|
||||
if (!display || !parentSurface) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: missing wl_display or "
|
||||
"parent wl_surface (display=%p surface=%p)\n",
|
||||
static_cast<void *>(display),
|
||||
static_cast<void *>(parentSurface));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PresenterGlobals *g = discoverGlobals(display);
|
||||
if (!g->compositor || !g->subcompositor) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: compositor lacks "
|
||||
"wl_compositor or wl_subcompositor (compositor=%p "
|
||||
"subcompositor=%p)\n",
|
||||
static_cast<void *>(g->compositor),
|
||||
static_cast<void *>(g->subcompositor));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
wl_surface *child = wl_compositor_create_surface(g->compositor);
|
||||
if (!child) return nullptr;
|
||||
|
||||
wl_subsurface *sub =
|
||||
wl_subcompositor_get_subsurface(g->subcompositor, child, parentSurface);
|
||||
if (!sub) {
|
||||
wl_surface_destroy(child);
|
||||
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);
|
||||
|
||||
// Subsurface covers the parent at the origin. Phase 3 will keep
|
||||
// this in sync on resize; for Phase 2 it doesn't matter because
|
||||
// we never attach a buffer.
|
||||
wl_subsurface_set_position(sub, 0, 0);
|
||||
|
||||
// Flush so the compositor sees the subsurface creation. We do NOT
|
||||
// commit the child surface — per protocol an uncommitted subsurface
|
||||
// with no attached buffer contributes nothing to the parent's
|
||||
// display, which is exactly the no-behavior-change state we want
|
||||
// for Phase 2.
|
||||
wl_display_flush(display);
|
||||
|
||||
if (int err = wl_display_get_error(display); err != 0) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: wl_display error %d after "
|
||||
"subsurface creation\n",
|
||||
err);
|
||||
wl_subsurface_destroy(sub);
|
||||
wl_surface_destroy(child);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: subsurface ready (parent=%p "
|
||||
"child=%p sub=%p)\n",
|
||||
static_cast<void *>(parentSurface),
|
||||
static_cast<void *>(child), static_cast<void *>(sub));
|
||||
|
||||
return std::unique_ptr<SubsurfacePresenter>(
|
||||
new SubsurfacePresenter(display, child, sub));
|
||||
}
|
||||
|
||||
SubsurfacePresenter::SubsurfacePresenter(wl_display *display, wl_surface *child,
|
||||
wl_subsurface *sub)
|
||||
: m_display(display), m_childSurface(child), m_subsurface(sub) {}
|
||||
|
||||
SubsurfacePresenter::~SubsurfacePresenter() {
|
||||
if (m_subsurface) wl_subsurface_destroy(m_subsurface);
|
||||
if (m_childSurface) wl_surface_destroy(m_childSurface);
|
||||
if (m_display) wl_display_flush(m_display);
|
||||
}
|
||||
|
||||
} // namespace wayland
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// Wayland subsurface presenter for `GhosttySurface`.
|
||||
//
|
||||
// Scaffolding for the GPU-direct present path (issue: Phase 2 of the
|
||||
// dmabuf-as-importable-surface rework). This class owns one
|
||||
// `wl_subsurface` parented to the `GhosttySurface`'s native
|
||||
// `wl_surface`. Its eventual job is to receive dmabuf fds from
|
||||
// libghostty's renderer, wrap each one in a `wl_buffer` via
|
||||
// `zwp_linux_dmabuf_v1`, and attach it to the subsurface so the
|
||||
// compositor scans it out directly — bypassing the current mmap +
|
||||
// memcpy + QImage + QPainter pipeline.
|
||||
//
|
||||
// In Phase 2 (this commit) the presenter only creates and tears down
|
||||
// the subsurface. No buffer is ever attached; the existing
|
||||
// `presentVulkanDmabuf` path keeps running unchanged. The proof this
|
||||
// scaffolding works is that `ghastty-vulkan` still launches and
|
||||
// renders identically with no Wayland protocol errors.
|
||||
//
|
||||
// Wayland-only by project decision (the Qt frontend is Wayland-only;
|
||||
// see `feedback-qt-no-x11` memory). If the host isn't on a Wayland
|
||||
// QPA platform or the compositor lacks `wl_subcompositor`,
|
||||
// `tryCreate` returns nullptr — Phase 2 silently ignores that
|
||||
// because nothing consumes the presenter yet; Phase 3 will treat it
|
||||
// as fatal.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
struct wl_display;
|
||||
struct wl_subsurface;
|
||||
struct wl_surface;
|
||||
class QWindow;
|
||||
|
||||
namespace wayland {
|
||||
|
||||
class SubsurfacePresenter {
|
||||
public:
|
||||
// Build a subsurface parented to `parent`'s native `wl_surface`.
|
||||
// Returns nullptr if any prerequisite is missing (non-Wayland QPA,
|
||||
// null `wl_display`, `wl_subcompositor` unbindable, etc.).
|
||||
//
|
||||
// Forces `Qt::WA_NativeWindow` on the caller is the *caller's*
|
||||
// responsibility — `tryCreate` only reads `parent->surfaceHandle`.
|
||||
static std::unique_ptr<SubsurfacePresenter> tryCreate(QWindow *parent);
|
||||
|
||||
~SubsurfacePresenter();
|
||||
|
||||
// Phase-3 accessors: when the present path moves to dmabuf-attach,
|
||||
// the caller will need the child `wl_surface` to attach buffers to
|
||||
// and the `wl_display` to flush. Exposed now so the API surface
|
||||
// doesn't churn between phases.
|
||||
wl_surface *childSurface() const { return m_childSurface; }
|
||||
wl_display *display() const { return m_display; }
|
||||
|
||||
SubsurfacePresenter(const SubsurfacePresenter &) = delete;
|
||||
SubsurfacePresenter &operator=(const SubsurfacePresenter &) = delete;
|
||||
|
||||
private:
|
||||
SubsurfacePresenter(wl_display *display, wl_surface *child,
|
||||
wl_subsurface *sub);
|
||||
|
||||
wl_display *m_display;
|
||||
wl_surface *m_childSurface;
|
||||
wl_subsurface *m_subsurface;
|
||||
};
|
||||
|
||||
} // namespace wayland
|
||||
Loading…
Reference in New Issue