qt/wayland: y-flip GL output via glBlitFramebuffer (Y_INVERT unsupported)

GL renders bottom-up (origin at bottom-left), Wayland/DRM samples
top-down — so the GL path's terminal pixels arrived upside-down at
the compositor. The linux-dmabuf-v1 protocol has Y_INVERT for
exactly this case, but KWin (and likely others) reject it with
"dma-buf flags are not supported".

Fix: keep m_fbo around for the GL path even when m_eglTarget
exists. Render + premultiply into m_fbo (normal GL orientation),
then glBlitFramebuffer m_fbo → m_eglTarget with an inverted
destination rect (y0=fbh, y1=0) which makes the blit flip
vertically while copying. Present m_eglTarget's dmabuf with
y_invert=false (we already did the flip ourselves).

Cost: a second FBO of equal size to m_eglTarget. ~12 MB extra
GPU memory at 1080p/HiDPI. Acceptable for visual correctness;
the blit itself is cheap on the GPU.

Added y_invert parameter to SubsurfacePresenter::presentDmabuf
(default false so the Vulkan path doesn't need changes — Vulkan
rasterizes Y-down natively). EglDmabufTarget exposes its raw FBO
id via framebuffer() so callers can glBlitFramebuffer into it.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-25 13:27:21 -05:00
parent 52d4ee4136
commit 230ee20629
4 changed files with 89 additions and 45 deletions

View File

@ -55,6 +55,7 @@
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLFramebufferObject>
#include <QOpenGLExtraFunctions>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
@ -309,24 +310,33 @@ void GhosttySurface::syncSurfaceSize() {
delete m_fbo;
m_fbo = nullptr;
// Prefer the dmabuf-backed target when the wl_subsurface presenter
// is up and EGL_MESA_image_dma_buf_export is available — the
// renderer draws directly into a texture whose memory is exported
// as a dmabuf, and we hand the fd straight to the compositor.
// When that's not available (no presenter, missing EGL extension,
// multi-plane export, etc.) we fall back to the legacy
// QOpenGLFramebufferObject + toImage + QPainter blit path.
// The GL path always renders into m_fbo first (regular GL_RGBA8
// FBO, GL's native bottom-left origin). When the subsurface
// presenter is up + EGL_MESA_image_dma_buf_export is available,
// we ALSO allocate m_eglTarget (a dmabuf-backed texture+FBO) and
// glBlitFramebuffer m_fbo → m_eglTarget with an inverted dst rect
// to flip Y on the way out — Wayland/DRM samples top-down, so
// without the flip the terminal would render upside-down. We
// can't use the linux-dmabuf-v1 Y_INVERT buffer flag because
// some compositors (KWin) reject it with "dma-buf flags are not
// supported".
//
// When m_eglTarget isn't available we fall back to the legacy
// m_fbo->toImage() + QPainter blit path (QImage handles its own
// Y flip).
QOpenGLFramebufferObjectFormat fmt;
fmt.setInternalTextureFormat(GL_RGBA8);
m_fbo = new QOpenGLFramebufferObject(QSize(w, h), fmt);
if (m_subsurfacePresenter) {
m_eglTarget = wayland::EglDmabufTarget::create(m_context, w, h);
if (m_eglTarget) {
m_useSubsurface.store(true, std::memory_order_release);
} else {
m_useSubsurface.store(false, std::memory_order_release);
}
}
if (!m_eglTarget) {
} else {
m_useSubsurface.store(false, std::memory_order_release);
QOpenGLFramebufferObjectFormat fmt;
fmt.setInternalTextureFormat(GL_RGBA8);
m_fbo = new QOpenGLFramebufferObject(QSize(w, h), fmt);
}
ghostty_surface_set_content_scale(m_surface, dpr, dpr);
@ -418,6 +428,10 @@ bool GhosttySurface::event(QEvent *e) {
if (m_surface) {
if (e->type() == QEvent::Show) {
ghostty_surface_set_occlusion(m_surface, true);
std::fprintf(stderr,
"[ghastty] Show surface=%p presenter=%p\n",
static_cast<void *>(this),
static_cast<void *>(m_subsurfacePresenter.get()));
// 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
@ -468,10 +482,16 @@ bool GhosttySurface::event(QEvent *e) {
// doesn't ghost on top of whatever the now-active tab is
// showing. The next Show + render reattaches a buffer and
// makes it visible again.
bool fpc = false;
if (m_subsurfacePresenter) {
m_subsurfacePresenter->hide();
forceParentCommit();
fpc = forceParentCommit();
}
std::fprintf(stderr,
"[ghastty] Hide surface=%p presenter=%p fpc=%d\n",
static_cast<void *>(this),
static_cast<void *>(m_subsurfacePresenter.get()),
fpc ? 1 : 0);
}
}
return QWidget::event(e);
@ -546,28 +566,44 @@ void GhosttySurface::renderTerminal() {
if (!makeCurrent()) return;
if (!m_eglTarget && !m_fbo) return;
// Two render-target variants:
// - EglDmabufTarget (zero-copy): libghostty draws into a
// dmabuf-backed texture; we hand the fd to the subsurface
// presenter and the compositor scans it out directly. No
// readback, no QPainter blit for the terminal pixels.
// - QOpenGLFramebufferObject (legacy): glReadPixels into a
// QImage, then paintEvent blits via QPainter. Used when the
// EGL dmabuf path isn't available.
if (m_eglTarget) {
m_eglTarget->bind();
m_context->functions()->glViewport(0, 0, m_fbw, m_fbh);
ghostty_surface_draw(m_surface);
premultiplyFramebuffer();
m_eglTarget->release();
if (m_subsurfacePresenter) {
m_subsurfacePresenter->presentDmabuf(
m_eglTarget->fd(), m_eglTarget->drmFormat(),
m_eglTarget->drmModifier(),
static_cast<quint32>(m_eglTarget->width()),
static_cast<quint32>(m_eglTarget->height()), m_eglTarget->stride(),
width(), height());
}
// Two output sinks. Both paths render into the same primary FBO
// first (m_fbo, regular GL_RGBA8, GL's native bottom-left origin).
// - EglDmabufTarget present (zero-copy): glBlitFramebuffer
// m_fbo into the dmabuf-backed FBO with an inverted dst rect
// to flip Y on the way out (Wayland/DRM samples top-down;
// the linux-dmabuf-v1 Y_INVERT buffer flag would do this
// compositor-side but KWin and others reject it as "dma-buf
// flags are not supported"). Hand the dmabuf to the
// subsurface presenter.
// - QImage fallback: glReadPixels into a QImage (which handles
// its own Y flip) and let paintEvent blit it via QPainter.
// Used when the EGL dmabuf path isn't available.
m_fbo->bind();
m_context->functions()->glViewport(0, 0, m_fbw, m_fbh);
ghostty_surface_draw(m_surface);
premultiplyFramebuffer();
if (m_eglTarget && m_subsurfacePresenter) {
// QOpenGLExtraFunctions exposes glBlitFramebuffer (GL 3.0+);
// QOpenGLFunctions doesn't. We pinned to OpenGL 4.3 elsewhere
// so the entry point is always available.
auto *xf = m_context->extraFunctions();
xf->glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo->handle());
xf->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_eglTarget->framebuffer());
// Inverted dst rect (y1 > y0) tells glBlitFramebuffer to flip
// vertically while copying. Matches the Y_INVERT semantic
// without needing compositor support for the flag.
xf->glBlitFramebuffer(0, 0, m_fbw, m_fbh,
0, m_fbh, m_fbw, 0,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
xf->glBindFramebuffer(GL_FRAMEBUFFER, 0);
m_subsurfacePresenter->presentDmabuf(
m_eglTarget->fd(), m_eglTarget->drmFormat(),
m_eglTarget->drmModifier(),
static_cast<quint32>(m_eglTarget->width()),
static_cast<quint32>(m_eglTarget->height()), m_eglTarget->stride(),
width(), height(),
/*y_invert*/ false);
// The terminal pixels reach the compositor via the subsurface,
// not via QPainter — but chrome (overlays, dim, bell flash)
// still goes through paintEvent. update() schedules that.
@ -575,13 +611,6 @@ void GhosttySurface::renderTerminal() {
return;
}
// libghostty renders into its own target and blits the result to the
// currently bound framebuffer — bind ours so we get the final image.
m_fbo->bind();
m_context->functions()->glViewport(0, 0, m_fbw, m_fbh);
ghostty_surface_draw(m_surface);
premultiplyFramebuffer();
// Read the frame back as a premultiplied, top-down QImage, tagged with
// the ratio the framebuffer was sized at so paintEvent can blit it 1:1
// at its true logical size. Using the live devicePixelRatioF() here

View File

@ -57,6 +57,12 @@ public:
std::uint32_t drmFormat() const { return m_drmFormat; }
std::uint64_t drmModifier() const { return m_drmModifier; }
std::uint32_t stride() const { return m_stride; }
// Raw GL framebuffer object id for glBlitFramebuffer callers that
// need to write into the dmabuf-backed FBO from a different
// attached target (e.g. blitting from m_fbo with an inverted dst
// rect to flip Y, since the linux-dmabuf-v1 Y_INVERT flag is not
// universally supported).
unsigned int framebuffer() const { return m_framebuffer; }
EglDmabufTarget(const EglDmabufTarget &) = delete;
EglDmabufTarget &operator=(const EglDmabufTarget &) = delete;

View File

@ -397,7 +397,8 @@ SubsurfacePresenter::~SubsurfacePresenter() {
void SubsurfacePresenter::presentDmabuf(int fd, uint32_t drm_format,
uint64_t drm_modifier, uint32_t width,
uint32_t height, uint32_t stride,
int dest_width, int dest_height) {
int dest_width, int dest_height,
bool y_invert) {
if (fd < 0 || !m_dmabuf || !m_childSurface || !m_viewport) return;
if (dest_width <= 0) dest_width = 1;
if (dest_height <= 0) dest_height = 1;
@ -410,9 +411,11 @@ void SubsurfacePresenter::presentDmabuf(int fd, uint32_t drm_format,
/*offset*/ 0, stride,
static_cast<uint32_t>(drm_modifier >> 32),
static_cast<uint32_t>(drm_modifier & 0xFFFFFFFFu));
const uint32_t buffer_flags =
y_invert ? ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT : 0;
wl_buffer *buffer = zwp_linux_buffer_params_v1_create_immed(
params, static_cast<int32_t>(width), static_cast<int32_t>(height),
drm_format, /*flags*/ 0);
drm_format, buffer_flags);
zwp_linux_buffer_params_v1_destroy(params);
if (!buffer) {
std::fprintf(stderr,

View File

@ -95,9 +95,15 @@ public:
// scale; for fractional scales they're independent (set via
// wp_viewport.set_destination, which decouples buffer dimensions
// from surface area).
// `y_invert` requests the compositor flip the buffer vertically
// when sampling. The OpenGL renderer's coordinate convention is
// bottom-left origin (Y up), but Wayland/DRM samples top-down —
// without the flag, GL frames render upside-down. Vulkan
// rasterizes Y-down by default and passes false.
void presentDmabuf(int fd, uint32_t drm_format, uint64_t drm_modifier,
uint32_t width, uint32_t height, uint32_t stride,
int dest_width, int dest_height);
int dest_width, int dest_height,
bool y_invert = false);
// Compositor-preferred fractional scale for this surface, in
// units of 1/120 (e.g. 144 = 1.2, 180 = 1.5, 240 = 2.0). Returns