[Feature] (DirectGLES): a stable canvas in front of the host frames

Every present hands the frame back to the display host and takes a different one, and the frame's
renderbuffer is deleted with it.  A compositor attaches its framebuffers once (kwin: one per daemon
buffer, for the life of the process), so attachments bound to a frame's own renderbuffer named a
deleted object from the second frame on.

The backend now keeps a canvas - a renderbuffer and framebuffer of its own, made at the first
frame's size and kept for the session.  The host-frame globals (the default-framebuffer redirect,
the storage external attachments are bound to) name the canvas instead of the frame, and keep
naming it between a frame being handed back and the next one arriving.  PresentHostFrame copies the
canvas into the frame before its fence; the copy puts back the compositor's framebuffer bindings,
scissor test and rasterizer discard exactly as they were.  Without a canvas the frame itself stays
the draw target, as before.
This commit is contained in:
BZLZHH
2026-10-01 09:47:24 -04:00
parent c88c6ff196
commit b72d4c5dff
2 changed files with 100 additions and 14 deletions
@@ -1095,17 +1095,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_hostFrameTarget = target;
m_hostFrameBuffer = frame;
m_hostFrameOffer = offer;
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
// as the draw target, which is how this path behaved before the canvas existed.
if (!EnsureHostFrameCanvas(target.Width, target.Height))
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
// resolve to this frame, and that resolution is what this global is.
g_hostFrameFramebufferId = m_hostFrameTarget.Framebuffer;
// Published together with the framebuffer, because they describe the same frame: the STORAGE
// an attachment is bound to when the client's texture has none of its own (the frame's own
// renderbuffer), and the SIZE that decides whether binding it is right at all.
g_hostFrameRenderbuffer = m_hostFrameTarget.Renderbuffer;
g_hostFrameWidth = m_hostFrameTarget.Width;
g_hostFrameHeight = m_hostFrameTarget.Height;
// resolve to this target, and that resolution is what this global is.
g_hostFrameFramebufferId = drawTarget.Framebuffer;
// Published together with the framebuffer, because they describe the same target: the STORAGE
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
// decides whether binding it is right at all.
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
g_hostFrameWidth = drawTarget.Width;
g_hostFrameHeight = drawTarget.Height;
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(m_hostFrameTarget);
HostFrameTargetBind(drawTarget);
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
// over there renders into THESE frames - it runs this project's own GBM, whose
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
@@ -1127,10 +1132,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
void BackendObject_DirectGLES::DestroyHostFrame() {
g_hostFrameFramebufferId = 0;
g_hostFrameRenderbuffer = 0;
g_hostFrameWidth = 0;
g_hostFrameHeight = 0;
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
// made over it stay valid between this frame being handed back and the next one arriving.
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
g_hostFrameWidth = m_hostFrameCanvas.Width;
g_hostFrameHeight = m_hostFrameCanvas.Height;
HostFrameTargetDestroy(m_hostFrameTarget);
if (m_hostFrameBuffer != nullptr) {
AHardwareBuffer_release(m_hostFrameBuffer);
@@ -1152,6 +1159,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// When this returns, the pixels are complete: glFinish is the fence the host is told
// about, and the host's own readback - which Complete reads - is what proves the drawing
// landed where the screen will look.
BlitHostFrameCanvas();
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
// server's apply loop passes through the host channel - the swap it runs lands here for a
@@ -1174,6 +1182,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
if (m_hostFrameCanvas.Framebuffer != 0) return true;
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
return false;
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
// framebuffer shadow and binds through it right after.
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
GLuint framebuffer = 0;
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
return false;
}
m_hostFrameCanvas.Renderbuffer = renderbuffer;
m_hostFrameCanvas.Framebuffer = framebuffer;
m_hostFrameCanvas.Width = width;
m_hostFrameCanvas.Height = height;
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
width, height, framebuffer, renderbuffer);
return true;
}
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
// shadow (which never saw this) is still right afterwards. The scissor test and
// rasterizer discard both apply to a blit, so they are lifted for it.
GLint readBinding = 0;
GLint drawBinding = 0;
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
static_cast<GLint>(m_hostFrameTarget.Width),
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
sameSize ? GL_NEAREST : GL_LINEAR);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
// The other arm's half: on a build with no host frames there is no frame to take, and the
@@ -1190,6 +1259,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
void BackendObject_DirectGLES::DestroyHostFrame() {}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
Bool BackendObject_DirectGLES::PresentHostFrame() {
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
@@ -1313,7 +1384,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// every draw lands in a pbuffer nobody sees.
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(m_hostFrameTarget);
HostFrameTargetBind(HostFrameDrawTarget());
}
return true;
}
@@ -69,6 +69,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Takes the next frame from the host and makes it this context's framebuffer.
Bool TakeHostFrame();
void DestroyHostFrame();
// The canvas the compositor draws into, made once at the first frame's size.
Bool EnsureHostFrameCanvas(Uint width, Uint height);
// Copies the canvas into the frame about to be presented.
void BlitHostFrameCanvas();
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
const HostFrameTarget& HostFrameDrawTarget() const {
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
}
void OnEGLSurfaceReleased(EGLSurface surface) override;
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
@@ -80,6 +88,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// descriptor this channel offers per frame and reads a release back from.
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
HostFrameTarget m_hostFrameTarget;
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
// attachments bound to a frame's own renderbuffer named a deleted object from the second
// frame on. The attachments name this instead, and each present copies it into the
// frame. No EGLImage behind it (Image stays null).
HostFrameTarget m_hostFrameCanvas;
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
MG_Backend::HostFrameOffer m_hostFrameOffer{};
Bool m_hostFrameBridgeOpen = false;