diff --git a/CMakeLists.txt b/CMakeLists.txt index 8575b6b6..4d945925 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -434,10 +434,13 @@ set(SOURCE_FILES MobileGL/MG_Backend/Init.cpp MobileGL/MG_Backend/BackendObject.cpp + MobileGL/MG_Backend/HostFrameBridge.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp + MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp b/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp new file mode 100644 index 00000000..0d1bf4c6 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp @@ -0,0 +1,205 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "HostFrameTarget.h" +#include "DirectGLES.h" + +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) + +#include +#include +#include + +namespace MobileGL::MG_Backend::DirectGLES { + namespace { + // The four entry points this is spelled in are optional everywhere: the loader's own + // tables do not carry them (a missing EGL function there is MGLOG_F), so the extension + // strings decide first and the pointers are taken only once they have. + using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*); + using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*); + using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR); + using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES); + + Bool HasExtensionWord(const char* list, const char* word) { + if (list == nullptr) return false; + const SizeT n = std::strlen(word); + for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) { + const Bool startOk = at == list || at[-1] == ' '; + const Bool endOk = !at[n] || at[n] == ' '; + if (startOk && endOk) return true; + } + return false; + } + + struct EntryPoints { + GetNativeClientBufferFn GetNativeClientBuffer = nullptr; + CreateImageFn CreateImage = nullptr; + DestroyImageFn DestroyImage = nullptr; + ImageTargetRenderbufferFn TargetRenderbuffer = nullptr; + }; + + // Resolved once per process and cached together with the refusal: a driver that lacks one + // of these does not grow it later, and the answer names what is missing. + const EntryPoints& Resolved(String& why) { + static EntryPoints points; + static Bool resolved = false; + static String refusal; + if (!resolved) { + resolved = true; + if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay || + !g_GLESFuncs.glGetString) { + refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString"; + } else { + const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS); + const char* gl = reinterpret_cast(g_GLESFuncs.glGetString(GL_EXTENSIONS)); + if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) { + refusal = "EGL_ANDROID_get_native_client_buffer is not advertised"; + } else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) { + refusal = "EGL_KHR_image_base is not advertised"; + } else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) { + refusal = "GL_OES_EGL_image is not advertised"; + } else { + points.GetNativeClientBuffer = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); + points.CreateImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR")); + points.DestroyImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR")); + points.TargetRenderbuffer = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES")); + if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr || + points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) { + refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / " + "eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES"; + points = EntryPoints{}; + } + } + } + } + why = refusal; + return points; + } + + void DrainGLErrors() { + if (!g_GLESFuncs.glGetError) return; + for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) { + } + } + } + + Bool HostFrameTargetSupported(String& why) { + const EntryPoints& points = Resolved(why); + return points.GetNativeClientBuffer != nullptr; + } + + Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) { + target = HostFrameTarget{}; + if (buffer == nullptr) { + why = "no buffer"; + return false; + } + const EntryPoints& points = Resolved(why); + if (points.GetNativeClientBuffer == nullptr) return false; + + if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers || + !g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer || + !g_GLESFuncs.glCheckFramebufferStatus) { + why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/" + "glFramebufferRenderbuffer/glCheckFramebufferStatus"; + return false; + } + + AHardwareBuffer_Desc desc{}; + AHardwareBuffer_describe(buffer, &desc); + + DrainGLErrors(); + const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer); + if (clientBuffer == nullptr) { + why = "eglGetNativeClientBufferANDROID returned null"; + return false; + } + const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; + const EGLImageKHR image = + points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, + clientBuffer, imageAttribs); + if (image == EGL_NO_IMAGE_KHR) { + why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame"; + return false; + } + + GLuint renderbuffer = 0; + g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast(image)); + const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR; + if (renderbufferError != GL_NO_ERROR) { + why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError); + g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer); + points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image); + return false; + } + + 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) { + why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status); + g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer); + g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer); + points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image); + return false; + } + + target.Image = image; + target.Renderbuffer = renderbuffer; + target.Framebuffer = framebuffer; + target.Width = desc.width; + target.Height = desc.height; + MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height, + framebuffer, desc.stride, desc.format); + return true; + } + + void HostFrameTargetBind(const HostFrameTarget& target) { + if (target.Framebuffer != 0 && g_GLESFuncs.glBindFramebuffer != nullptr) + g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, target.Framebuffer); + } + + void HostFrameTargetDestroy(HostFrameTarget& target) { + if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers) + g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer); + if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers) + g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer); + if (target.Image != nullptr) { + String why; + const EntryPoints& points = Resolved(why); + if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay) + points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast(target.Image)); + } + target = HostFrameTarget{}; + } +} + +#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__) + +namespace MobileGL::MG_Backend::DirectGLES { + Bool HostFrameTargetSupported(String& why) { + why = "host frames are a disaggregated Android path"; + return false; + } + Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) { + why = "host frames are a disaggregated Android path"; + return false; + } + void HostFrameTargetBind(const HostFrameTarget&) {} + void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; } +} + +#endif diff --git a/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h b/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h new file mode 100644 index 00000000..52f6cf09 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h @@ -0,0 +1,52 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include +#include +#include + +struct AHardwareBuffer; + +namespace MobileGL::MG_Backend::DirectGLES { + // THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET. + // + // The display host (the Android side that owns the screen) allocates its frames itself and + // offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured + // on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side + // allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames + // a render server can draw into are the ones the host made for it. + // + // What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a + // framebuffer that is the drawing target while that frame is the current one. None of + // Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is + // the point: the host puts the frame on the glass when this side says it is drawn. + struct HostFrameTarget { + void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types + Uint Renderbuffer = 0; + Uint Framebuffer = 0; + Uint Width = 0; + Uint Height = 0; + }; + + // Whether the driver advertises what this is spelled in. False is answered with the missing + // name in `why`, never an abort: a server that cannot take host frames has to be able to + // say so. + Bool HostFrameTargetSupported(String& why); + + // Imports `buffer` as a render target. On failure `target` is left empty and `why` says + // which call refused; no GL name is leaked. + Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why); + + // Binds the target as the current draw framebuffer, called when its frame becomes current. + void HostFrameTargetBind(const HostFrameTarget& target); + + // Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which + // releases it; this never touches it. + void HostFrameTargetDestroy(HostFrameTarget& target); +} diff --git a/MobileGL/MG_Backend/HostFrameBridge.cpp b/MobileGL/MG_Backend/HostFrameBridge.cpp new file mode 100644 index 00000000..37091418 --- /dev/null +++ b/MobileGL/MG_Backend/HostFrameBridge.cpp @@ -0,0 +1,181 @@ +// MobileGL - MobileGL/MG_Backend/HostFrameBridge.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "HostFrameBridge.h" + +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) + +#include +#include +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Backend { + namespace { + Bool ReadExactly(Int fd, void* out, SizeT n) { + Uint8* at = static_cast(out); + while (n > 0) { + const ssize_t got = ::read(fd, at, n); + if (got <= 0) { + if (got < 0 && errno == EINTR) continue; + return false; + } + at += got; + n -= static_cast(got); + } + return true; + } + + Bool WriteExactly(Int fd, const void* in, SizeT n) { + const Uint8* at = static_cast(in); + while (n > 0) { + const ssize_t put = ::write(fd, at, n); + if (put <= 0) { + if (put < 0 && errno == EINTR) continue; + return false; + } + at += put; + n -= static_cast(put); + } + return true; + } + } + + HostFrameBridge::~HostFrameBridge() { Close(); } + + Bool HostFrameBridge::Open(const char* path, Int acceptTimeoutMs, String& why) { + Close(); + const char* name = (path != nullptr && path[0] != 0) ? path : kHostFrameSocket; + + m_listen = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (m_listen < 0) { + why = std::format("socket(AF_UNIX): {}", std::strerror(errno)); + return false; + } + sockaddr_un address{}; + address.sun_family = AF_UNIX; + if (std::strlen(name) >= sizeof(address.sun_path)) { + why = std::format("the socket path is longer than the kernel takes: {}", name); + Close(); + return false; + } + std::strncpy(address.sun_path, name, sizeof(address.sun_path) - 1); + // A leftover socket file from a server that died is not a reason to refuse the host. + ::unlink(name); + if (::bind(m_listen, reinterpret_cast(&address), sizeof(address)) != 0) { + why = std::format("bind({}): {}", name, std::strerror(errno)); + Close(); + return false; + } + if (::listen(m_listen, 1) != 0) { + why = std::format("listen({}): {}", name, std::strerror(errno)); + Close(); + return false; + } + + pollfd waiting{}; + waiting.fd = m_listen; + waiting.events = POLLIN; + const Int ready = ::poll(&waiting, 1, acceptTimeoutMs); + if (ready <= 0) { + why = (ready == 0) ? std::format("no display host dialled {} within {} ms", name, acceptTimeoutMs) + : std::format("poll({}): {}", name, std::strerror(errno)); + Close(); + return false; + } + m_fd = ::accept(m_listen, nullptr, nullptr); + if (m_fd < 0) { + why = std::format("accept({}): {}", name, std::strerror(errno)); + Close(); + return false; + } + MGLOG_I("host frame bridge: a display host is on %s", name); + return true; + } + + Bool HostFrameBridge::Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why) { + *buffer = nullptr; + offer = HostFrameOffer{}; + if (m_fd < 0) { + why = "the bridge is not open"; + return false; + } + AHardwareBuffer* received = nullptr; + if (AHardwareBuffer_recvHandleFromUnixSocket(m_fd, &received) != 0 || received == nullptr) { + why = "the host stopped offering frames, or the handle did not cross"; + return false; + } + if (!ReadExactly(m_fd, &offer, sizeof(offer)) || offer.Magic != kHostFrameMagic) { + // The handle is already this side's by the time this fails, so it goes back before + // the failure does: a reference held past the connection starves the host's pool. + AHardwareBuffer_release(received); + why = std::format("the frame's description did not follow its handle (magic 0x{:08x})", offer.Magic); + return false; + } + *buffer = received; + return true; + } + + Bool HostFrameBridge::Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why) { + if (m_fd < 0) { + why = "the bridge is not open"; + return false; + } + HostFrameAck ack{}; + ack.Magic = kHostFrameMagic; + ack.Version = kHostFrameVersion; + ack.Index = offer.Index; + ack.Imported = imported ? 1u : 0u; + ack.Drawn = drawn ? 1u : 0u; + ack.FenceOk = fenceOk ? 1u : 0u; + if (!WriteExactly(m_fd, &ack, sizeof(ack))) { + why = std::format("the answer to frame {} did not go out: {}", offer.Index, std::strerror(errno)); + return false; + } + // The host answers with its own view of the pixels. It is what closes the round trip, so + // it is read here and kept rather than left in the socket for the next frame to trip over. + m_lastSeen = HostFrameSeen{}; + if (!ReadExactly(m_fd, &m_lastSeen, sizeof(m_lastSeen)) || m_lastSeen.Magic != kHostFrameMagic) { + why = std::format("frame {} was answered but the host's readback did not arrive", offer.Index); + m_lastSeen = HostFrameSeen{}; + return false; + } + return true; + } + + void HostFrameBridge::Close() { + if (m_fd >= 0) ::close(m_fd); + if (m_listen >= 0) ::close(m_listen); + m_fd = -1; + m_listen = -1; + } +} + +#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__) + +namespace MobileGL::MG_Backend { + HostFrameBridge::~HostFrameBridge() = default; + Bool HostFrameBridge::Open(const char*, Int, String& why) { + why = "host frames are a disaggregated Android path"; + return false; + } + Bool HostFrameBridge::Acquire(struct AHardwareBuffer**, HostFrameOffer&, String& why) { + why = "host frames are a disaggregated Android path"; + return false; + } + Bool HostFrameBridge::Complete(const HostFrameOffer&, Bool, Bool, Bool, String& why) { + why = "host frames are a disaggregated Android path"; + return false; + } + void HostFrameBridge::Close() {} +} + +#endif diff --git a/MobileGL/MG_Backend/HostFrameBridge.h b/MobileGL/MG_Backend/HostFrameBridge.h new file mode 100644 index 00000000..cca3945e --- /dev/null +++ b/MobileGL/MG_Backend/HostFrameBridge.h @@ -0,0 +1,99 @@ +// MobileGL - MobileGL/MG_Backend/HostFrameBridge.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +struct AHardwareBuffer; + +namespace MobileGL::MG_Backend { + // THE DISPLAY HOST'S FRAMES, AS THEY ARRIVE. + // + // The Android side that owns the screen allocates its frames itself and offers each one over + // an AF_UNIX socket: the handle travels ahead of a fixed-size description on the same + // connection, and the host then waits for this side to say the frame is drawn before it puts + // it on the glass. Why the host allocates rather than this side asking for the display's own + // buffers is measured, not chosen: AHardwareBuffer_createFromHandle accepts an + // AHardwareBuffer this process allocated and refuses one the display queue handed over + // (gralloc BAD_VALUE), so the only frames a render server can draw into are the ones the host + // made for it. + // + // The three message shapes below ARE the host's (its ahb_bridge.h), field for field and + // packing included. They are restated here rather than shared because the two ends are built + // by different projects; a change on either side is a protocol change, and the magic is what + // makes a mismatched pair fail loudly instead of being read as plausible numbers. + inline constexpr Uint32 kHostFrameMagic = 0x4D474C41u; // the host spells it "ALGM" in memory + inline constexpr Uint32 kHostFrameVersion = 1u; + inline constexpr const char* kHostFrameSocket = "/data/local/tmp/mobilegl_bridge.sock"; + + struct HostFrameOffer { + Uint32 Magic; + Uint32 Version; + Uint32 Index; + Uint32 Width; + Uint32 Height; + Uint32 StrideBytes; + Uint32 Format; + Uint32 UsageLo; + Uint32 UsageHi; + char Note[64]; + } __attribute__((packed)); + + struct HostFrameAck { + Uint32 Magic; + Uint32 Version; + Uint32 Index; + Uint32 Imported; + Uint32 Drawn; + Uint32 FenceOk; + Uint8 Color[4]; + char Note[64]; + } __attribute__((packed)); + + struct HostFrameSeen { + Uint32 Magic; + Uint32 Version; + Uint32 Index; + Uint32 Locked; + Uint8 Observed[4]; + Uint8 ObservedCorner[4]; + char Note[64]; + } __attribute__((packed)); + + // One host connection at a time, which is what the host offers: it dials, hands over a frame, + // waits for the answer, and hands over the next. + class HostFrameBridge { + public: + HostFrameBridge() = default; + HostFrameBridge(const HostFrameBridge&) = delete; + HostFrameBridge& operator=(const HostFrameBridge&) = delete; + ~HostFrameBridge(); + + // Listens on `path` (the well-known name by default) and waits for the host. A host that + // never dials is answered with false and a reason, never an abort. + Bool Open(const char* path, Int acceptTimeoutMs, String& why); + + // Takes the next frame. Ownership of the returned AHardwareBuffer is the caller's, who + // must hand it back through ReleaseOwned() once the frame is off the glass. + Bool Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why); + + // Tells the host what became of the frame it offered, and reads back what the host's own + // view of the pixels holds. `drawn` is what the host waits for before it presents. + Bool Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why); + + // The readback the host returned for the last completed frame, as the host reported it. + const HostFrameSeen& LastSeen() const { return m_lastSeen; } + + void Close(); + + private: + Int m_fd = -1; + Int m_listen = -1; + HostFrameSeen m_lastSeen{}; + }; +}