qt/wayland: OpenGL renderer presents via subsurface too
New wayland::EglDmabufTarget allocates a GL_RGBA8 texture, wraps it as an EGLImage via eglCreateImage, exports its memory as a dmabuf via eglExportDMABUFImageMESA, and attaches the texture to a GL framebuffer for libghostty's GL renderer to draw into. The cached fd / fourcc / modifier / stride feed straight into SubsurfacePresenter::presentDmabuf — same compositor path the Vulkan renderer uses, just sourced from EGL instead of Vulkan. GhosttySurface (GL path) builds the target in syncSurfaceSize when the wl_subsurface presenter is up and the EGL display advertises EGL_MESA_image_dma_buf_export; falls back to the existing QOpenGLFramebufferObject + toImage + QPainter blit otherwise. renderTerminal routes to either target. paintEvent already gates its blit on m_useSubsurface so the new path skips the readback entirely. The initial syncSurfaceSize fires before QEvent::Show, when the presenter doesn't exist yet — so it takes the legacy branch. event(Show) now invalidates m_fbw on the GL path and re-runs syncSurfaceSize once the presenter comes up, giving the target a second chance to materialize. Verified on NVIDIA RTX 2080 + KDE Wayland: GL build picks fourcc=AB24 (ABGR8888, matches GL_RGBA8 byte order on LE) with a vendor-tiled modifier (0x300000000e08014), no wl_display protocol errors, frames flow via the subsurface. Multi-plane exports are refused at create-time (the present-callback ABI is single-plane). Single-plane vendor-tiled is the common case on RGBA, but multi-plane would need a wider ABI to land cleanly. Co-Authored-By: claude-flow <ruv@ruv.net>pull/12846/head
parent
33560fe83e
commit
07ab0de7d4
|
|
@ -53,6 +53,11 @@ find_package(LayerShellQt REQUIRED)
|
|||
# QPA native-handle accessors.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
||||
# libEGL for the OpenGL present path's dmabuf export
|
||||
# (EGL_MESA_image_dma_buf_export). Resolved at runtime via
|
||||
# eglGetProcAddress, so we only need the link for the base entry
|
||||
# points (eglQueryString, eglGetCurrentDisplay, eglGetError).
|
||||
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
||||
# libxkbcommon: derive the unshifted Unicode codepoint for a key event
|
||||
# from its XKB keycode, so libghostty's kitty encoder finds an entry for
|
||||
# punctuation keys (Qt's ev->key() reports the SHIFTED symbol, e.g.
|
||||
|
|
@ -162,6 +167,7 @@ add_executable(ghastty
|
|||
src/TabWidget.cpp
|
||||
src/undo/UndoStack.cpp
|
||||
src/Util.cpp
|
||||
src/wayland/EglDmabufTarget.cpp
|
||||
src/wayland/SubsurfacePresenter.cpp
|
||||
src/WindowBlur.cpp
|
||||
src/XkbTracker.cpp
|
||||
|
|
@ -193,6 +199,7 @@ target_link_libraries(ghastty PRIVATE
|
|||
Qt6::Svg
|
||||
PkgConfig::WAYLAND_CLIENT
|
||||
PkgConfig::XKBCOMMON
|
||||
PkgConfig::EGL
|
||||
LayerShellQt::Interface
|
||||
vulkan
|
||||
"${GHOSTTY_LINK_SO}"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "TabWidget.h"
|
||||
#include "Util.h"
|
||||
#include "vulkan/Host.h"
|
||||
#include "wayland/EglDmabufTarget.h"
|
||||
#include "wayland/SubsurfacePresenter.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
|
@ -255,10 +256,29 @@ void GhosttySurface::syncSurfaceSize() {
|
|||
}
|
||||
|
||||
if (!makeCurrent()) return;
|
||||
m_eglTarget.reset();
|
||||
delete m_fbo;
|
||||
QOpenGLFramebufferObjectFormat fmt;
|
||||
fmt.setInternalTextureFormat(GL_RGBA8);
|
||||
m_fbo = new QOpenGLFramebufferObject(QSize(w, h), fmt);
|
||||
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.
|
||||
if (m_subsurfacePresenter) {
|
||||
m_eglTarget = wayland::EglDmabufTarget::create(m_context, w, h);
|
||||
if (m_eglTarget) {
|
||||
m_useSubsurface.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
if (!m_eglTarget) {
|
||||
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);
|
||||
ghostty_surface_set_size(m_surface, static_cast<uint32_t>(w),
|
||||
|
|
@ -324,13 +344,26 @@ bool GhosttySurface::event(QEvent *e) {
|
|||
if (auto *h = windowHandle()) {
|
||||
m_subsurfacePresenter =
|
||||
wayland::SubsurfacePresenter::tryCreate(h);
|
||||
if (m_subsurfacePresenter && 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);
|
||||
if (m_subsurfacePresenter) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -407,7 +440,39 @@ void GhosttySurface::renderTerminal() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!m_fbo || !makeCurrent()) return;
|
||||
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) {
|
||||
const int scale =
|
||||
std::max(1, static_cast<int>(std::lround(devicePixelRatioF())));
|
||||
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(),
|
||||
scale);
|
||||
}
|
||||
// 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.
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// libghostty renders into its own target and blits the result to the
|
||||
// currently bound framebuffer — bind ours so we get the final image.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
namespace wayland {
|
||||
class SubsurfacePresenter;
|
||||
class EglDmabufTarget;
|
||||
}
|
||||
|
||||
class MainWindow;
|
||||
|
|
@ -245,6 +246,14 @@ private:
|
|||
QOpenGLContext *m_context = nullptr;
|
||||
QOffscreenSurface *m_offscreen = nullptr;
|
||||
QOpenGLFramebufferObject *m_fbo = nullptr;
|
||||
// Dmabuf-exporting GL target (zero-copy path). Set when the EGL
|
||||
// display advertises EGL_MESA_image_dma_buf_export and the
|
||||
// wl_subsurface presenter is up; the renderer draws into this
|
||||
// texture-backed framebuffer and we attach its fd straight to the
|
||||
// subsurface — no glReadPixels, no QImage, no QPainter blit.
|
||||
// Stays null when EGL support is missing or the subsurface failed
|
||||
// to bring up, and the legacy m_fbo path runs as fallback.
|
||||
std::unique_ptr<wayland::EglDmabufTarget> m_eglTarget;
|
||||
QImage m_image; // last frame, read back from m_fbo
|
||||
|
||||
// True when this surface is using the Vulkan platform. The
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
#include "EglDmabufTarget.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QOpenGLContext>
|
||||
#include <QOpenGLFunctions>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
namespace wayland {
|
||||
|
||||
namespace {
|
||||
|
||||
// EGL_MESA_image_dma_buf_export entry points (loaded once per
|
||||
// process). Resolved via `eglGetProcAddress`, which returns null if
|
||||
// the extension isn't present.
|
||||
using PFNeglExportDMABUFImageQueryMESA =
|
||||
EGLBoolean (*)(EGLDisplay dpy, EGLImageKHR image, int *fourcc,
|
||||
int *num_planes, EGLuint64KHR *modifiers);
|
||||
using PFNeglExportDMABUFImageMESA =
|
||||
EGLBoolean (*)(EGLDisplay dpy, EGLImageKHR image, int *fds,
|
||||
EGLint *strides, EGLint *offsets);
|
||||
|
||||
struct EglFns {
|
||||
PFNEGLCREATEIMAGEKHRPROC createImage = nullptr;
|
||||
PFNEGLDESTROYIMAGEKHRPROC destroyImage = nullptr;
|
||||
PFNeglExportDMABUFImageQueryMESA queryExport = nullptr;
|
||||
PFNeglExportDMABUFImageMESA exportImage = nullptr;
|
||||
bool resolved = false;
|
||||
bool available = false;
|
||||
};
|
||||
|
||||
EglFns &eglFns() {
|
||||
static EglFns f;
|
||||
return f;
|
||||
}
|
||||
|
||||
bool ensureEglFns(EGLDisplay display) {
|
||||
EglFns &f = eglFns();
|
||||
if (f.resolved) return f.available;
|
||||
f.resolved = true;
|
||||
|
||||
const char *exts = eglQueryString(display, EGL_EXTENSIONS);
|
||||
if (!exts) return false;
|
||||
auto hasExt = [exts](const char *name) {
|
||||
const std::size_t n = std::strlen(name);
|
||||
const char *p = exts;
|
||||
while ((p = std::strstr(p, name)) != nullptr) {
|
||||
if ((p == exts || p[-1] == ' ') && (p[n] == '\0' || p[n] == ' '))
|
||||
return true;
|
||||
p += n;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!hasExt("EGL_KHR_image_base") ||
|
||||
!hasExt("EGL_MESA_image_dma_buf_export")) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: EGL display lacks "
|
||||
"EGL_KHR_image_base or EGL_MESA_image_dma_buf_export\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
f.createImage = reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(
|
||||
eglGetProcAddress("eglCreateImageKHR"));
|
||||
f.destroyImage = reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(
|
||||
eglGetProcAddress("eglDestroyImageKHR"));
|
||||
f.queryExport = reinterpret_cast<PFNeglExportDMABUFImageQueryMESA>(
|
||||
eglGetProcAddress("eglExportDMABUFImageQueryMESA"));
|
||||
f.exportImage = reinterpret_cast<PFNeglExportDMABUFImageMESA>(
|
||||
eglGetProcAddress("eglExportDMABUFImageMESA"));
|
||||
if (!f.createImage || !f.destroyImage || !f.queryExport ||
|
||||
!f.exportImage) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: eglGetProcAddress returned "
|
||||
"null for required entry points\n");
|
||||
return false;
|
||||
}
|
||||
f.available = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
EGLDisplay currentEglDisplay() {
|
||||
return eglGetCurrentDisplay();
|
||||
}
|
||||
|
||||
// GL constants come from <QOpenGLFunctions> indirectly via the Qt
|
||||
// GL headers — GL_TEXTURE_2D / GL_RGBA8 / GL_FRAMEBUFFER etc. are
|
||||
// in scope without further includes.
|
||||
|
||||
} // namespace
|
||||
|
||||
bool EglDmabufTarget::available(QOpenGLContext *ctx) {
|
||||
if (!ctx) return false;
|
||||
if (!ctx->isValid()) return false;
|
||||
EGLDisplay dpy = currentEglDisplay();
|
||||
if (dpy == EGL_NO_DISPLAY) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
"[ghastty] EglDmabufTarget: no current EGL display (call after "
|
||||
"QOpenGLContext::makeCurrent on a Wayland QPA)\n");
|
||||
return false;
|
||||
}
|
||||
return ensureEglFns(dpy);
|
||||
}
|
||||
|
||||
std::unique_ptr<EglDmabufTarget> EglDmabufTarget::create(QOpenGLContext *ctx,
|
||||
int width_px,
|
||||
int height_px) {
|
||||
if (!ctx || !ctx->isValid()) return nullptr;
|
||||
if (width_px <= 0 || height_px <= 0) return nullptr;
|
||||
EGLDisplay dpy = currentEglDisplay();
|
||||
if (dpy == EGL_NO_DISPLAY) return nullptr;
|
||||
if (!ensureEglFns(dpy)) return nullptr;
|
||||
const EglFns &fns = eglFns();
|
||||
auto *gl = ctx->functions();
|
||||
if (!gl) return nullptr;
|
||||
|
||||
auto target = std::unique_ptr<EglDmabufTarget>(new EglDmabufTarget());
|
||||
target->m_eglDisplay = dpy;
|
||||
target->m_width = width_px;
|
||||
target->m_height = height_px;
|
||||
|
||||
// 1. Allocate a GL texture sized to the desired framebuffer.
|
||||
unsigned int tex = 0;
|
||||
gl->glGenTextures(1, &tex);
|
||||
if (tex == 0) return nullptr;
|
||||
gl->glBindTexture(GL_TEXTURE_2D, tex);
|
||||
gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width_px, height_px, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
gl->glBindTexture(GL_TEXTURE_2D, 0);
|
||||
target->m_texture = tex;
|
||||
|
||||
// 2. Wrap as an EGLImage targeting the GL texture.
|
||||
EGLImageKHR img = fns.createImage(
|
||||
dpy, ctx->nativeInterface<QNativeInterface::QEGLContext>()
|
||||
? reinterpret_cast<EGLContext>(
|
||||
ctx->nativeInterface<QNativeInterface::QEGLContext>()
|
||||
->nativeContext())
|
||||
: eglGetCurrentContext(),
|
||||
EGL_GL_TEXTURE_2D_KHR,
|
||||
reinterpret_cast<EGLClientBuffer>(static_cast<uintptr_t>(tex)), nullptr);
|
||||
if (img == EGL_NO_IMAGE_KHR) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: eglCreateImageKHR failed (0x%x)\n",
|
||||
eglGetError());
|
||||
gl->glDeleteTextures(1, &tex);
|
||||
return nullptr;
|
||||
}
|
||||
target->m_eglImage = img;
|
||||
|
||||
// 3. Query the export metadata (fourcc, plane count, modifier).
|
||||
int fourcc = 0;
|
||||
int num_planes = 0;
|
||||
EGLuint64KHR modifier = 0;
|
||||
if (!fns.queryExport(dpy, img, &fourcc, &num_planes, &modifier)) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: eglExportDMABUFImageQueryMESA "
|
||||
"failed (0x%x)\n",
|
||||
eglGetError());
|
||||
return nullptr;
|
||||
}
|
||||
if (num_planes != 1) {
|
||||
// Multi-plane modifiers need a wider present-callback ABI on the
|
||||
// subsurface side. NVIDIA / Mesa default tilings for RGBA are
|
||||
// single-plane in practice; refuse multi-plane cleanly and fall
|
||||
// back to the QImage path.
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: refusing multi-plane export "
|
||||
"(num_planes=%d fourcc=0x%x mod=0x%llx)\n",
|
||||
num_planes, fourcc,
|
||||
static_cast<unsigned long long>(modifier));
|
||||
return nullptr;
|
||||
}
|
||||
target->m_drmFormat = static_cast<std::uint32_t>(fourcc);
|
||||
target->m_drmModifier = static_cast<std::uint64_t>(modifier);
|
||||
|
||||
// 4. Export the dmabuf fd + per-plane stride/offset.
|
||||
int fd = -1;
|
||||
EGLint stride = 0;
|
||||
EGLint offset = 0;
|
||||
if (!fns.exportImage(dpy, img, &fd, &stride, &offset) || fd < 0) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: eglExportDMABUFImageMESA failed "
|
||||
"(0x%x fd=%d)\n",
|
||||
eglGetError(), fd);
|
||||
return nullptr;
|
||||
}
|
||||
target->m_fd = fd;
|
||||
target->m_stride = static_cast<std::uint32_t>(stride);
|
||||
|
||||
// 5. Attach to a framebuffer so libghostty can render into it.
|
||||
unsigned int fbo = 0;
|
||||
gl->glGenFramebuffers(1, &fbo);
|
||||
if (fbo == 0) {
|
||||
::close(fd);
|
||||
target->m_fd = -1;
|
||||
return nullptr;
|
||||
}
|
||||
target->m_framebuffer = fbo;
|
||||
gl->glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, tex, 0);
|
||||
const unsigned int status = gl->glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
gl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: framebuffer incomplete (0x%x)\n",
|
||||
status);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[ghastty] EglDmabufTarget: %dx%d fd=%d fourcc=0x%x mod=0x%llx "
|
||||
"stride=%u\n",
|
||||
width_px, height_px, fd, target->m_drmFormat,
|
||||
static_cast<unsigned long long>(target->m_drmModifier),
|
||||
target->m_stride);
|
||||
return target;
|
||||
}
|
||||
|
||||
EglDmabufTarget::EglDmabufTarget() = default;
|
||||
|
||||
EglDmabufTarget::~EglDmabufTarget() {
|
||||
// Caller must ensure the owning QOpenGLContext is current; on
|
||||
// GhosttySurface destruction we go through `makeCurrent` first.
|
||||
auto ctx = QOpenGLContext::currentContext();
|
||||
if (ctx) {
|
||||
auto *gl = ctx->functions();
|
||||
if (m_framebuffer) gl->glDeleteFramebuffers(1, &m_framebuffer);
|
||||
if (m_texture) gl->glDeleteTextures(1, &m_texture);
|
||||
}
|
||||
if (m_eglImage && m_eglDisplay) {
|
||||
eglFns().destroyImage(m_eglDisplay, m_eglImage);
|
||||
}
|
||||
if (m_fd >= 0) ::close(m_fd);
|
||||
}
|
||||
|
||||
void EglDmabufTarget::bind() const {
|
||||
auto ctx = QOpenGLContext::currentContext();
|
||||
if (!ctx || !m_framebuffer) return;
|
||||
ctx->functions()->glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
|
||||
}
|
||||
|
||||
void EglDmabufTarget::release() const {
|
||||
auto ctx = QOpenGLContext::currentContext();
|
||||
if (!ctx) return;
|
||||
ctx->functions()->glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
} // namespace wayland
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Dmabuf-exporting GL render target for the OpenGL present path.
|
||||
//
|
||||
// libghostty's GL renderer draws into a host-owned framebuffer (see
|
||||
// GhosttySurface's `m_fbo`). Today that framebuffer's pixels get
|
||||
// pulled back through `glReadPixels` (via `QOpenGLFramebufferObject::toImage`)
|
||||
// into a QImage, then re-uploaded to the QWidget backing store by
|
||||
// QPainter. After this class is wired in, the host instead allocates
|
||||
// a GL texture, wraps it as an `EGLImage` via `eglCreateImage`,
|
||||
// exports its memory as a dmabuf via `eglExportDMABUFImageMESA`,
|
||||
// and attaches that texture to a GL framebuffer for libghostty to
|
||||
// draw into. The cached dmabuf fd / fourcc / modifier / stride are
|
||||
// then handed straight to the `wayland::SubsurfacePresenter` — same
|
||||
// zero-copy path the Vulkan renderer's Target uses, just sourced
|
||||
// from EGL instead of Vulkan.
|
||||
//
|
||||
// Requires `EGL_MESA_image_dma_buf_export` (checked by the static
|
||||
// `available()` predicate). Wayland-only by project decision.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class QOpenGLContext;
|
||||
|
||||
namespace wayland {
|
||||
|
||||
class EglDmabufTarget {
|
||||
public:
|
||||
// Detect at runtime whether the current EGL display advertises
|
||||
// `EGL_MESA_image_dma_buf_export`. Caller MUST have a Wayland QPA
|
||||
// and `ctx` must be a usable, makeCurrent-able QOpenGLContext.
|
||||
// Cached after first call.
|
||||
static bool available(QOpenGLContext *ctx);
|
||||
|
||||
// Build a target of the given device-pixel size. Returns nullptr
|
||||
// on any EGL / GL failure (caller falls back to the legacy
|
||||
// QOpenGLFramebufferObject + toImage path). `ctx` must be current
|
||||
// on the calling thread when called.
|
||||
static std::unique_ptr<EglDmabufTarget> create(QOpenGLContext *ctx,
|
||||
int width_px,
|
||||
int height_px);
|
||||
|
||||
~EglDmabufTarget();
|
||||
|
||||
// Bind the framebuffer for draw operations. Caller is responsible
|
||||
// for `glViewport` / `glClear` etc. Mirrors `QOpenGLFramebufferObject::bind`.
|
||||
void bind() const;
|
||||
void release() const;
|
||||
|
||||
// Pixel + dmabuf metadata. Stable for the lifetime of this target;
|
||||
// resize allocates a new target. `stride` is the value returned by
|
||||
// `eglExportDMABUFImageMESA` for plane 0.
|
||||
int width() const { return m_width; }
|
||||
int height() const { return m_height; }
|
||||
int fd() const { return m_fd; }
|
||||
std::uint32_t drmFormat() const { return m_drmFormat; }
|
||||
std::uint64_t drmModifier() const { return m_drmModifier; }
|
||||
std::uint32_t stride() const { return m_stride; }
|
||||
|
||||
EglDmabufTarget(const EglDmabufTarget &) = delete;
|
||||
EglDmabufTarget &operator=(const EglDmabufTarget &) = delete;
|
||||
|
||||
private:
|
||||
EglDmabufTarget();
|
||||
|
||||
// Opaque to callers (and avoids leaking EGL/GL handle types into
|
||||
// the header). The .cpp owns the EGLDisplay/EGLImage casts.
|
||||
void *m_eglDisplay = nullptr;
|
||||
void *m_eglImage = nullptr;
|
||||
unsigned int m_texture = 0;
|
||||
unsigned int m_framebuffer = 0;
|
||||
int m_width = 0;
|
||||
int m_height = 0;
|
||||
int m_fd = -1;
|
||||
std::uint32_t m_drmFormat = 0;
|
||||
std::uint64_t m_drmModifier = 0;
|
||||
std::uint32_t m_stride = 0;
|
||||
};
|
||||
|
||||
} // namespace wayland
|
||||
Loading…
Reference in New Issue