From c7ab618ab0a3d9eb0085f36980510db9e9114cdc Mon Sep 17 00:00:00 2001 From: gold Date: Thu, 1 Oct 2026 00:57:14 +0800 Subject: [PATCH] [Feature] (disaggregated): the host's frames as a draw target, and the socket they arrive on Two modules, and nothing else changes yet: they are the half of the host-framed surface that is about the frame itself, kept apart from the surface lifecycle so they can be reviewed and built on their own. HostFrameTarget (DirectGLES): an AHardwareBuffer becomes a renderbuffer through an EGLImage and a framebuffer that is the drawing target while its frame is current. The extension strings decide first and the four entry points are taken from eglGetProcAddress only after they have, which is the shape the T0 AHB import in Managers.cpp already uses; a driver that lacks one of them is answered with its name, never an abort. Every failure path drops what it made, so no GL name and no EGLImage outlives a refusal. HostFrameBridge (MG_Backend): the display host's own protocol, restated field for field - a handle ahead of a fixed-size description on one AF_UNIX connection, then the answer, then the host's readback of the pixels it can see. The host dials; this side listens on the well-known name. A handle that arrives without its description is released before the failure is reported, because a reference held past the connection starves the pool the host draws its frames from. Why the host allocates the frames at all is measured, not chosen: AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this process allocated and refuses one the display queue handed over (gralloc BAD_VALUE) - same process, same descriptor, same handle shape. That result is what these two modules are built around, and it is written down where the next reader will look for it. Both compile against this tree's own flags. Wiring them into the surface lifecycle, the swap path and the client's opt-in is the next step, not this one. --- CMakeLists.txt | 3 + .../MG_Backend/DirectGLES/HostFrameTarget.cpp | 205 ++++++++++++++++++ .../MG_Backend/DirectGLES/HostFrameTarget.h | 52 +++++ MobileGL/MG_Backend/HostFrameBridge.cpp | 181 ++++++++++++++++ MobileGL/MG_Backend/HostFrameBridge.h | 99 +++++++++ 5 files changed, 540 insertions(+) create mode 100644 MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h create mode 100644 MobileGL/MG_Backend/HostFrameBridge.cpp create mode 100644 MobileGL/MG_Backend/HostFrameBridge.h 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{}; + }; +}