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
parent
52d4ee4136
commit
230ee20629
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue