qt/wayland: share top-level wl_surface for all pane subsurfaces
Splits rendered black because forcing WA_NativeWindow on each
GhosttySurface gave every pane its own QWindow, which Qt couldn't
shell cleanly inside a QSplitter ("QWidgetWindow must be a top
level window" warning). The presenter would create a wl_subsurface
parented to that broken child QWindow's wl_surface; subsurface
existed and frames flowed but nothing made it to the screen.
Refactor: every pane's wl_subsurface attaches to the TOP-LEVEL
QWindow's wl_surface as a sibling, positioned at the pane's
offset within the top-level. Removes WA_NativeWindow from
GhosttySurface; uses window()->windowHandle() to reach the
top-level QWindow. SubsurfacePresenter:
- tryCreate now expects the top-level QWindow
- setPosition(x, y) lets the caller update the pane offset
- state cached: only emits the wayland request when the value
actually changes
GhosttySurface:
- drops setAttribute(WA_NativeWindow)
- Show handler walks window()->windowHandle() instead of own
- sets initial subsurface position to mapTo(window(), QPoint(0,0))
- new moveEvent override updates position when the splitter
divider drags or sibling panes resize
- resizeEvent also re-emits position because shifting siblings
move us implicitly
- forceParentCommit now commits the top-level QWindow's QPA
(was committing the per-pane one, now nonexistent)
This is the architectural fix the researcher 3 path led to —
without per-pane native QWindows, the QSplitter shenanigans go
away entirely, and the wl_subsurface protocol's sibling-stacking
under one parent does exactly what we need.
Per-surface first-frame log (m_loggedFirstFrame) kept as
diagnostic for future pane lifecycle issues.
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
4ef76eda78
commit
ef4df3b8da
|
|
@ -59,6 +59,7 @@
|
|||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QPainter>
|
||||
#include <QMoveEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QSplitter>
|
||||
#include <QString>
|
||||
|
|
@ -90,16 +91,15 @@ GhosttySurface::GhosttySurface(ghostty_app_t app, MainWindow *owner,
|
|||
// The widget paints a per-pixel-alpha QImage of the terminal; a
|
||||
// translucent background lets that alpha reach the desktop.
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
// Force a native QWindow + wl_surface for this widget from the
|
||||
// start. Required for the wl_subsurface presenter (it parents
|
||||
// its child surface to our windowHandle()'s wl_surface). Setting
|
||||
// this in the ctor — not in the Show handler — guarantees that
|
||||
// by the time the first PlatformSurface / Show event fires, the
|
||||
// windowHandle is established. For split panes that get re-
|
||||
// parented into a QSplitter, the Show event flow can otherwise
|
||||
// race with WA_NativeWindow taking effect and leave the
|
||||
// presenter never created.
|
||||
setAttribute(Qt::WA_NativeWindow);
|
||||
// NOTE: deliberately NOT calling setAttribute(Qt::WA_NativeWindow).
|
||||
// Forcing a per-pane native QWindow caused Qt to complain
|
||||
// ("QWidgetWindow must be a top level window") and rendered
|
||||
// split panes black: Qt's QSplitter-embedded child widgets can't
|
||||
// be shelled cleanly on Wayland. Instead, every GhosttySurface
|
||||
// shares the top-level QWindow's wl_surface (got via
|
||||
// `window()->windowHandle()` in the Show handler). Each pane's
|
||||
// wl_subsurface attaches to that shared parent, positioned at
|
||||
// the pane's offset within the top-level via `setPosition`.
|
||||
|
||||
// Pick the renderer up-front so the rest of the surface setup
|
||||
// (GL context vs. Vulkan host) only touches the path we'll
|
||||
|
|
@ -335,9 +335,29 @@ void GhosttySurface::syncSurfaceSize() {
|
|||
renderTerminal();
|
||||
}
|
||||
|
||||
void GhosttySurface::moveEvent(QMoveEvent *) {
|
||||
// When the splitter divider drags or a new pane gets inserted,
|
||||
// our offset within the top-level changes. Update the
|
||||
// wl_subsurface position so the terminal pixels follow the
|
||||
// widget.
|
||||
if (m_subsurfacePresenter && window()) {
|
||||
const QPoint pos = mapTo(window(), QPoint(0, 0));
|
||||
m_subsurfacePresenter->setPosition(pos.x(), pos.y());
|
||||
forceParentCommit();
|
||||
}
|
||||
}
|
||||
|
||||
void GhosttySurface::resizeEvent(QResizeEvent *) {
|
||||
layoutScrollbar();
|
||||
syncSurfaceSize();
|
||||
// Resize can also shift our position within the top-level (e.g.
|
||||
// a sibling pane growing pushes us right). Update position too.
|
||||
if (m_subsurfacePresenter && window()) {
|
||||
const QPoint pos = mapTo(window(), QPoint(0, 0));
|
||||
m_subsurfacePresenter->setPosition(pos.x(), pos.y());
|
||||
// forceParentCommit happens inside syncSurfaceSize's
|
||||
// drainVulkan/renderTerminal path, so we don't double up here.
|
||||
}
|
||||
if (m_exitOverlay) m_exitOverlay->setGeometry(rect());
|
||||
if (m_keySeqOverlay && m_keySeqOverlay->isVisible())
|
||||
m_keySeqOverlay->move(8, height() - m_keySeqOverlay->height() - 8);
|
||||
|
|
@ -410,36 +430,32 @@ bool GhosttySurface::event(QEvent *e) {
|
|||
// one producing pixels. Phase 3 will route frames through the
|
||||
// subsurface and retire the QPainter blit.
|
||||
if (!m_subsurfacePresenter) {
|
||||
// WA_NativeWindow was set in the ctor, so windowHandle()
|
||||
// should be non-null by now. If it isn't, we log and try
|
||||
// again on the next Show — the window may genuinely not
|
||||
// have a surface yet (very early in the show cycle).
|
||||
QWindow *h = windowHandle();
|
||||
if (!h) {
|
||||
// Use the TOP-LEVEL QWindow's wl_surface as the parent for
|
||||
// our subsurface — NOT this widget's own QWindow. Each pane
|
||||
// in a split is a sibling subsurface under the same
|
||||
// top-level wl_surface, positioned via setPosition. This
|
||||
// avoids forcing WA_NativeWindow on embedded children
|
||||
// (which made Qt unhappy with QSplitter).
|
||||
QWindow *top = window() ? window()->windowHandle() : nullptr;
|
||||
if (!top) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] GhosttySurface::event(Show): "
|
||||
"windowHandle() is null, will retry next show\n");
|
||||
"top-level windowHandle() is null, will retry "
|
||||
"next show\n");
|
||||
}
|
||||
if (h) {
|
||||
if (top) {
|
||||
m_subsurfacePresenter =
|
||||
wayland::SubsurfacePresenter::tryCreate(h);
|
||||
wayland::SubsurfacePresenter::tryCreate(top);
|
||||
if (m_subsurfacePresenter) {
|
||||
// Set initial position to our offset within the top-level.
|
||||
// moveEvent updates it on layout changes.
|
||||
const QPoint pos = mapTo(window(), QPoint(0, 0));
|
||||
m_subsurfacePresenter->setPosition(pos.x(), pos.y());
|
||||
if (m_useVulkan) {
|
||||
// Flip the Vulkan present path over to the zero-copy
|
||||
// wl_subsurface route. Release-style store pairs with
|
||||
// the renderer thread's acquire-load — once it
|
||||
// observes true, it stops parking QImages and just
|
||||
// hands us the dmabuf descriptor for compositor
|
||||
// handoff.
|
||||
m_useSubsurface.store(true, std::memory_order_release);
|
||||
} else {
|
||||
// OpenGL path: re-sync the framebuffer so
|
||||
// syncSurfaceSize can build an EglDmabufTarget.
|
||||
// syncSurfaceSize's initial call ran *before* this
|
||||
// Show — m_subsurfacePresenter was null then, so it
|
||||
// took the legacy QOpenGLFramebufferObject branch.
|
||||
// Invalidate the cached size so the early-return at
|
||||
// the top of syncSurfaceSize doesn't bail.
|
||||
m_fbw = m_fbh = -1;
|
||||
syncSurfaceSize();
|
||||
}
|
||||
|
|
@ -1545,17 +1561,17 @@ void GhosttySurface::presentVulkanDmabuf(
|
|||
const bool useSubsurface =
|
||||
image_backed && m_useSubsurface.load(std::memory_order_acquire);
|
||||
|
||||
// One-shot breadcrumb so logs confirm the dmabuf hand-off is
|
||||
// wired. Subsequent frames are silent so we don't spam stderr.
|
||||
static bool logged_first = false;
|
||||
if (!logged_first) {
|
||||
logged_first = true;
|
||||
// Per-surface one-shot breadcrumb so logs confirm the dmabuf
|
||||
// hand-off is wired for each pane/split independently. Subsequent
|
||||
// frames are silent so we don't spam stderr.
|
||||
if (!m_loggedFirstFrame) {
|
||||
m_loggedFirstFrame = true;
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] first Vulkan dmabuf frame: fd=%d %ux%u stride=%u "
|
||||
"fourcc=0x%08x mod=0x%lx image_backed=%d path=%s\n",
|
||||
dmabuf_fd, width, height, stride, drm_format,
|
||||
static_cast<unsigned long>(drm_modifier), image_backed ? 1 : 0,
|
||||
useSubsurface ? "subsurface" : "qimage");
|
||||
"[ghastty] first dmabuf for surface=%p: fd=%d %ux%u "
|
||||
"stride=%u fourcc=0x%08x mod=0x%lx image_backed=%d path=%s\n",
|
||||
static_cast<void *>(this), dmabuf_fd, width, height, stride,
|
||||
drm_format, static_cast<unsigned long>(drm_modifier),
|
||||
image_backed ? 1 : 0, useSubsurface ? "subsurface" : "qimage");
|
||||
}
|
||||
|
||||
if (dmabuf_fd < 0 || width == 0 || height == 0 || stride < width * 4)
|
||||
|
|
@ -1648,16 +1664,18 @@ void GhosttySurface::drainVulkan() {
|
|||
}
|
||||
|
||||
bool GhosttySurface::forceParentCommit() {
|
||||
// Get the QPA implementation for our QWindow. On Wayland this is
|
||||
// QtWaylandClient::QWaylandWindow (private API, hence the
|
||||
// Qt6::WaylandClientPrivate link). Calling commit() on it flushes
|
||||
// Qt's pending wl_surface state plus any queued client requests —
|
||||
// crucially including the cached wl_subsurface state from our
|
||||
// sync-mode child commit, which applies atomically with this
|
||||
// parent commit.
|
||||
QWindow *handle = windowHandle();
|
||||
if (!handle) return false;
|
||||
QPlatformWindow *qpa = handle->handle();
|
||||
// Commit the TOP-LEVEL QWindow's wl_surface — the parent of our
|
||||
// wl_subsurface. We do NOT have a per-pane native QWindow (see
|
||||
// ctor comment about WA_NativeWindow), so windowHandle() on this
|
||||
// widget is null; reach the top-level via `window()->windowHandle()`.
|
||||
//
|
||||
// QtWaylandClient::QWaylandWindow is Qt's private QPA impl
|
||||
// (Qt6::WaylandClientPrivate). Calling commit() on it flushes
|
||||
// Qt's pending wl_surface state plus any cached child subsurface
|
||||
// state from our sync-mode commits.
|
||||
QWindow *top = window() ? window()->windowHandle() : nullptr;
|
||||
if (!top) return false;
|
||||
QPlatformWindow *qpa = top->handle();
|
||||
if (!qpa) return false;
|
||||
auto *wl = dynamic_cast<QtWaylandClient::QWaylandWindow *>(qpa);
|
||||
if (!wl) return false;
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ protected:
|
|||
bool event(QEvent *) override;
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void resizeEvent(QResizeEvent *) override;
|
||||
void moveEvent(QMoveEvent *) override;
|
||||
|
||||
// Disable Qt's Tab/Backtab focus traversal so those keys reach
|
||||
// keyPressEvent and can be forwarded to the terminal.
|
||||
|
|
@ -368,4 +369,7 @@ private:
|
|||
// 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;
|
||||
// Per-surface latch for the first-dmabuf log breadcrumb so each
|
||||
// pane / split prints its own line on first frame.
|
||||
bool m_loggedFirstFrame = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -204,8 +204,8 @@ std::size_t supportedDmabufModifiers(std::uint32_t drm_format,
|
|||
}
|
||||
|
||||
std::unique_ptr<SubsurfacePresenter>
|
||||
SubsurfacePresenter::tryCreate(QWindow *parent) {
|
||||
if (!parent) return nullptr;
|
||||
SubsurfacePresenter::tryCreate(QWindow *topLevel) {
|
||||
if (!topLevel) return nullptr;
|
||||
|
||||
if (!QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
|
||||
std::fprintf(stderr,
|
||||
|
|
@ -219,7 +219,7 @@ SubsurfacePresenter::tryCreate(QWindow *parent) {
|
|||
auto *display = static_cast<wl_display *>(
|
||||
native->nativeResourceForIntegration("wl_display"));
|
||||
auto *parentSurface = static_cast<wl_surface *>(
|
||||
native->nativeResourceForWindow("surface", parent));
|
||||
native->nativeResourceForWindow("surface", topLevel));
|
||||
if (!display || !parentSurface) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] SubsurfacePresenter: missing wl_display or "
|
||||
|
|
@ -474,4 +474,17 @@ void SubsurfacePresenter::resizeDestination(int dest_width, int dest_height) {
|
|||
wl_display_flush(m_display);
|
||||
}
|
||||
|
||||
void SubsurfacePresenter::setPosition(int x, int y) {
|
||||
if (!m_subsurface) return;
|
||||
if (x == m_lastX && y == m_lastY) return;
|
||||
wl_subsurface_set_position(m_subsurface, x, y);
|
||||
m_lastX = x;
|
||||
m_lastY = y;
|
||||
// Position is double-buffered on the parent surface — the caller
|
||||
// must trigger a parent commit (forceParentCommit on the GhosttySurface
|
||||
// side) for the change to land. We flush so the request is on the
|
||||
// wire when that happens.
|
||||
wl_display_flush(m_display);
|
||||
}
|
||||
|
||||
} // namespace wayland
|
||||
|
|
|
|||
|
|
@ -61,15 +61,20 @@ std::size_t supportedDmabufModifiers(std::uint32_t drm_format,
|
|||
|
||||
class SubsurfacePresenter {
|
||||
public:
|
||||
// Build a subsurface parented to `parent`'s native `wl_surface`,
|
||||
// and bind the linux-dmabuf-v1 global on the same display.
|
||||
// Build a subsurface parented to `topLevel`'s native `wl_surface`,
|
||||
// and bind the linux-dmabuf-v1 global on the same display. Pass
|
||||
// the TOP-LEVEL QWindow (e.g. `widget->window()->windowHandle()`)
|
||||
// — NOT a per-widget native QWindow. We attach all panes/splits
|
||||
// as siblings under the top-level surface and position each with
|
||||
// `setPosition`, instead of giving each pane its own QWindow
|
||||
// (which Qt's QSplitter-embedded child widgets don't handle
|
||||
// cleanly: "QWidgetWindow must be a top level window" warning,
|
||||
// and the result renders black).
|
||||
//
|
||||
// Returns nullptr if any prerequisite is missing (non-Wayland QPA,
|
||||
// null `wl_display`, `wl_subcompositor` unbindable,
|
||||
// `zwp_linux_dmabuf_v1` unbindable, etc.).
|
||||
//
|
||||
// Forcing `Qt::WA_NativeWindow` on the caller is the *caller's*
|
||||
// responsibility — `tryCreate` only reads `parent->surfaceHandle`.
|
||||
static std::unique_ptr<SubsurfacePresenter> tryCreate(QWindow *parent);
|
||||
static std::unique_ptr<SubsurfacePresenter> tryCreate(QWindow *topLevel);
|
||||
|
||||
~SubsurfacePresenter();
|
||||
|
||||
|
|
@ -120,6 +125,15 @@ public:
|
|||
// subsurface during resize.
|
||||
void resizeDestination(int dest_width, int dest_height);
|
||||
|
||||
// Update the subsurface position in parent-surface-local coords.
|
||||
// For panes inside splits / tabs, position is the GhosttySurface
|
||||
// widget's offset within the top-level (`mapTo(window(),
|
||||
// QPoint(0,0))`). wl_subsurface.set_position is double-buffered
|
||||
// on the *parent* surface — caller must trigger a parent commit
|
||||
// (Qt's QtWaylandClient::QWaylandWindow::commit()) for the new
|
||||
// position to apply. No-op if the position hasn't changed.
|
||||
void setPosition(int x, int y);
|
||||
|
||||
// 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.
|
||||
|
|
@ -144,6 +158,8 @@ private:
|
|||
uint32_t m_preferredScale120 = 120; // default: 1.0×
|
||||
int m_lastDestWidth = 0;
|
||||
int m_lastDestHeight = 0;
|
||||
int m_lastX = 0;
|
||||
int m_lastY = 0;
|
||||
};
|
||||
|
||||
} // namespace wayland
|
||||
|
|
|
|||
Loading…
Reference in New Issue