[Fix] (EGL, SharedImage): real EGL native fences publish a dma-buf producer's writes, so Chrome's GPU process frames reach the compositor.

This commit is contained in:
2026-10-03 10:59:02 -04:00
parent 2a1889b817
commit 4f6f4125ac
26 changed files with 790 additions and 28 deletions
+16
View File
@@ -674,6 +674,15 @@ namespace MobileGL {
// overwrite what another session still samples). A backend without shared images has
// nothing to publish.
virtual Bool PublishSharedImageAccesses() { return true; }
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
// (the caller's to close) that signals once everything the session submitted so far has
// completed - and, for a session that writes images, what PublishSharedImageAccesses
// does. -1 when there is none to give: the backend cannot export one, or the work has
// already completed - in both cases it HAS completed when this returns.
virtual Bool ExportNativeFence(int* fence) {
*fence = -1;
return PublishSharedImageAccesses();
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
@@ -723,6 +732,13 @@ namespace MobileGL {
// returning once the fence is published. Skipped (true) when nothing reached the stream
// since the last one.
virtual Bool FlushSharedImageAccesses() { return false; }
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
// caller's to close), or -1 when it had none - the fenced work has then completed.
virtual Bool CreateNativeFence(int* fence) {
*fence = -1;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
@@ -1238,6 +1238,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) {
*fence = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::ExportNativeFence(fence);
#else
return true;
#endif
}
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
@@ -53,6 +53,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: each window surface on it swaps its native EGL
@@ -18882,6 +18882,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
Bool ExportNativeFence(int* fence) {
*fence = -1;
ReadFrame& frame = CurrentReadFrame();
// No context current: nothing of this session is in flight to fence.
if (t_boundNativeContext == EGL_NO_CONTEXT) return true;
TextureImpl::WritePendingSharedImageUploads();
int exported = -1;
if (!ExportFence(&exported)) {
// No descriptor to give: the work is waited out, so the caller may report it done.
exported = -1;
(void)WaitForCompletion();
}
// A session that writes images (one that ends frames at flushes) ends one here too; a
// compositor's reads stay for its swap, which comes after the fence's work anyway.
if (frame.ImplicitSync && frame.Tracker.Pending() != 0) {
frame.Tracker.PublishFrameAsWrite(exported >= 0 ? ::dup(exported) : -1);
frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
}
*fence = exported;
return true;
}
void PublishPendingReads() {
ReadFrame& frame = CurrentReadFrame();
if (frame.Tracker.Pending() == 0) return;
@@ -19078,6 +19100,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void AcquireForSampling(const EglImageRef& image) { (void)image; }
void PublishPendingReads() {}
Bool PublishPendingAccesses() { return true; }
Bool ExportNativeFence(int* fence) {
*fence = -1;
return true;
}
void ForgetPresentTargetsAfterTerminate() {}
#endif
} // namespace SharedImageImpl
@@ -551,6 +551,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// fence, and the session's first use of an image in each frame from now on also waits for
// the image's pending reads.
Bool PublishPendingAccesses();
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): a
// sync_file of everything the current context submitted in `*fence` (-1: none, and the work
// is waited out here), and - for a session that writes images - the boundary
// PublishPendingAccesses makes.
Bool ExportNativeFence(int* fence);
} // namespace SharedImageImpl
#endif
@@ -594,6 +594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif
}
Bool BackendObject_DirectVulkan::ExportNativeFence(int* fence) {
*fence = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return pVulkanRenderer ? pVulkanRenderer->ExportNativeFence(fence) : true;
#else
return true;
#endif
}
// A swapchain image keeps its content through a present whatever region is drawn next: the
// damage region the client may declare is not needed here.
Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge(Bool damageRegionFollows) {
@@ -44,6 +44,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// A glFlush that ends a shared-image producer's frame (VulkanRenderer::PublishSharedImageAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (VulkanRenderer::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: the window surface's target (VkSurfaceKHR,
@@ -196,6 +196,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the exportable semaphore, and its sync_file is published to each as a write and a read.
// From then on an image's first acquire per frame also waits for its pending reads.
Bool PublishSharedImageAccesses();
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): the
// submission's sync_file in `*fence` (-1: none, and the work is waited out here), and - for
// a session that writes images - the boundary PublishSharedImageAccesses makes.
Bool ExportNativeFence(int* fence);
// EGL_BUFFER_AGE_EXT of the active target's default framebuffer for the next frame: the age
// of the swapchain image acquired for it (SwapchainObject::BufferAgeOf). Asking makes the
// target keep its presented images' content from then on.
@@ -726,8 +730,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// m_sharedImageSyncFd: the device imports and exports SYNC_FD binary semaphores; without
// it the waits happen on the CPU and nothing is exported (see WireSharedImage.inc).
Bool m_sharedImageSyncFd = false;
// The session ends frames at publishing flushes (PublishSharedImageAccesses): it writes the
// images it acquires, so a first acquire waits for the image's pending reads as well.
// The session writes images - it ends frames at publishing flushes
// (PublishSharedImageAccesses), or it reached one through a framebuffer (ResolveWireImage):
// a first acquire waits for the image's pending reads as well, and its boundaries publish
// what it used as written.
Bool m_sharedImageImplicitSync = false;
PFN_vkImportSemaphoreFdKHR m_importSemaphoreFd = nullptr;
PFN_vkGetSemaphoreFdKHR m_getSemaphoreFd = nullptr;
@@ -161,7 +161,12 @@
auto* resource = m_textureManager->SyncTextureResourceByHandle(out.storage, renderbuffer);
if (!resource) MagmaWireFatal("framebuffer-resource");
// Another session writes it: acquired (WireSharedImage.inc) before the layout is read below.
if (resource->sharedImageId != 0) AcquireSharedImage(*resource);
// An image reached through a framebuffer is one this session renders into (or copies
// through), which makes it a writer of images from now on.
if (resource->sharedImageId != 0) {
m_sharedImageImplicitSync = true;
AcquireSharedImage(*resource);
}
out.image = resource->image;
out.format = viewFormat != VK_FORMAT_UNDEFINED ? viewFormat : resource->format;
out.extent = {std::max(resource->extent.width >> level, 1u),
@@ -625,6 +625,47 @@
return true;
}
// THE NATIVE FENCE. A sync_file of everything the session submitted so far, for the client's
// EGL_ANDROID_native_fence_sync: the application hands the descriptor on (Chrome's GPU process,
// to the browser that commits the buffer), so it has to signal no earlier than the last of that
// work. With nothing recorded, an empty
// recording is submitted to carry the signal: the in-order queue runs it after every earlier
// submission, and it is tracked like any other (the semaphore's reuse waits for it).
Bool VulkanRenderer::ExportNativeFence(int* fence) {
*fence = -1;
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0)
return true; // nothing was ever submitted: nothing to fence
// A session that writes images ends its frame here, as at a flush; a compositor that only
// samples them keeps its reads for its present, which follows the fence's work anyway.
const Bool boundary = m_sharedImageImplicitSync;
if (boundary) ReleaseHeldSharedImages();
if (m_sharedImageSyncFd && m_sharedImageSignal == VK_NULL_HANDLE)
m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true);
m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
if (!HasPendingRecordedWork() && m_sharedImageSignal != VK_NULL_HANDLE)
m_frameContext.BeginCommandRecording();
const Bool submitted = HasPendingRecordedWork() && FlushPendingCommands();
if (HasPendingRecordedWork()) MGLOG_E_ONCE("Magma: a native fence's commands could not be submitted");
const VkSemaphore signal =
submitted ? std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE) : VK_NULL_HANDLE;
int exported = -1;
if (signal == VK_NULL_HANDLE || !ExportSharedImageSignal(signal, exported)) exported = -1;
if (exported < 0) {
// No descriptor: the work is waited out, so the caller may report it completed.
if (!WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX))
MGLOG_E_ONCE("Magma: a native fence's work could not be waited for");
}
if (boundary && m_sharedImageReads.Pending() != 0) {
int published = -1;
#if !defined(_WIN32)
if (exported >= 0) published = ::dup(exported);
#endif
m_sharedImageReads.PublishFrameAsWrite(published);
}
*fence = exported;
return true;
}
void VulkanRenderer::PublishSharedImageReads() {
const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE);
int fence = -1;
+40
View File
@@ -1529,9 +1529,49 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SYNC;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// EGL_ANDROID_native_fence_sync's fence command: a native fence with no descriptor of the
// application's. The server publishes the current context's shared-image accesses - its
// writes into dma-bufs become visible to the compositor's session, as at a glFlush - and
// hands back a sync_file of the work, which the application passes on with the buffer
// (Chrome's GPU process creates one per frame instead of flushing, when the compositor's
// kernel takes dma-buf fences). Answered through the round trip, so it is flushed too.
if (type == EGL_SYNC_NATIVE_FENCE_ANDROID && SharedImagesAvailable()) {
Bool hasFd = false;
for (const EGLAttrib* at = attrib_list; at != nullptr && at[0] != EGL_NONE; at += 2) {
if (at[0] == EGL_SYNC_NATIVE_FENCE_FD_ANDROID && at[1] != EGL_NO_NATIVE_FENCE_FD_ANDROID) hasFd = true;
}
if (!hasFd) {
if (GetCurrentContext() == EGL_NO_CONTEXT) {
state->SetError(EGL_BAD_MATCH);
return EGL_NO_SYNC;
}
int fence = -1;
{
const GLStreamScope stream;
MG_Pipe::MGPipeClientDrainTextureUploads();
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (backendObject == nullptr || !backendObject->CreateNativeFence(&fence)) {
MGLOG_E_ONCE("eglCreateSync: the server did not answer a native fence; none is created");
state->SetError(EGL_BAD_ALLOC);
return EGL_NO_SYNC;
}
}
return state->CreateNativeFenceSync(dpy, fence, EGL_SYNC_PRIOR_COMMANDS_COMPLETE);
}
}
#endif
return state->CreateSync(dpy, type, attrib_list);
}
EGLint DupNativeFenceFD(EGLDisplay dpy, EGLSync sync) {
auto* state = GetState();
if (!state) {
return EGL_NO_NATIVE_FENCE_FD_ANDROID;
}
return state->DupNativeFenceFD(dpy, sync);
}
EGLBoolean DestroySync(EGLDisplay dpy, EGLSync sync) {
auto* state = GetState();
if (!state) {
+2
View File
@@ -56,6 +56,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLBoolean DestroySync(EGLDisplay dpy, EGLSync sync);
EGLint ClientWaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
EGLBoolean GetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib* value);
// EGL_ANDROID_native_fence_sync: a copy of a native fence sync's sync_file.
EGLint DupNativeFenceFD(EGLDisplay dpy, EGLSync sync);
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attrib_list);
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image);
@@ -37,7 +37,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS
" EGL_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers"
" EGL_EXT_buffer_age EGL_KHR_partial_update EGL_KHR_swap_buffers_with_damage"
" EGL_EXT_swap_buffers_with_damage";
" EGL_EXT_swap_buffers_with_damage"
// Native fences come from the server's sessions (a sync_file of a session's submitted work),
// which is also what publishes a context's writes into dma-bufs to the compositor without a
// swap: Chrome's GPU process ends each frame with one rather than a flush.
" EGL_KHR_fence_sync EGL_KHR_wait_sync EGL_ANDROID_native_fence_sync";
inline constexpr const char* kClientExtensionString =
"EGL_EXT_client_extensions EGL_KHR_create_context EGL_EXT_platform_base "
"EGL_KHR_platform_base EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
@@ -287,8 +287,10 @@ MOBILEGL_EGL_API EGLBoolean eglDestroySync(EGLDisplay dpy, EGLSync sync) {
return MobileGL::MG_Impl::EGLImpl::DestroySync(dpy, sync);
}
// The waits take no stream lock: they read EGL state only (its own lock), never the stream, and a
// native fence's wait blocks for as long as the fenced work runs - with the lock held, every other
// thread's GL call would wait with it.
MOBILEGL_EGL_API EGLint eglClientWaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglClientWaitSync(dpy=%p, sync=%p, flags=%d, timeout=%llu)", dpy, sync, flags,
static_cast<unsigned long long>(timeout));
return MobileGL::MG_Impl::EGLImpl::ClientWaitSync(dpy, sync, flags, timeout);
@@ -300,6 +302,65 @@ MOBILEGL_EGL_API EGLBoolean eglGetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLin
return MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, sync, attribute, value);
}
// EGL_KHR_fence_sync / EGL_KHR_wait_sync / EGL_ANDROID_native_fence_sync's spellings, which
// ANGLE's GLES-on-EGL backend loads by name (and only through them offers fences to Chrome): the
// same objects as EGL 1.5's, with EGLint attribute lists.
namespace {
// EGL_KHR_fence_sync's attribute list widened to EGL 1.5's; at most 16 pairs, as no sync type
// takes more than two attributes.
bool WidenSyncAttribs(const EGLint* in, EGLAttrib* out, int capacity) {
int n = 0;
for (const EGLint* at = in; at != nullptr && at[0] != EGL_NONE; at += 2) {
if (n + 3 > capacity) return false;
out[n++] = static_cast<EGLAttrib>(at[0]);
out[n++] = static_cast<EGLAttrib>(at[1]);
}
out[n] = EGL_NONE;
return true;
}
} // namespace
MOBILEGL_EGL_API EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglCreateSyncKHR(dpy=%p, type=%u, attrib_list=%p)", dpy, type, attrib_list);
EGLAttrib attribs[33];
if (!WidenSyncAttribs(attrib_list, attribs, 33)) return EGL_NO_SYNC_KHR;
return MobileGL::MG_Impl::EGLImpl::CreateSync(dpy, type, attribs);
}
MOBILEGL_EGL_API EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglDestroySyncKHR(dpy=%p, sync=%p)", dpy, sync);
return MobileGL::MG_Impl::EGLImpl::DestroySync(dpy, sync);
}
MOBILEGL_EGL_API EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout) {
MGLOG_D("eglClientWaitSyncKHR(dpy=%p, sync=%p, flags=%d, timeout=%llu)", dpy, sync, flags,
static_cast<unsigned long long>(timeout));
return MobileGL::MG_Impl::EGLImpl::ClientWaitSync(dpy, sync, flags, timeout);
}
MOBILEGL_EGL_API EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglGetSyncAttribKHR(dpy=%p, sync=%p, attribute=%d, value=%p)", dpy, sync, attribute, value);
if (value == nullptr) return MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, sync, attribute, nullptr);
EGLAttrib wide = 0;
const EGLBoolean ok = MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, sync, attribute, &wide);
if (ok == EGL_TRUE) *value = static_cast<EGLint>(wide);
return ok;
}
MOBILEGL_EGL_API EGLint eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags) {
MGLOG_D("eglWaitSyncKHR(dpy=%p, sync=%p, flags=%d)", dpy, sync, flags);
return MobileGL::MG_Impl::EGLImpl::WaitSync(dpy, sync, flags) == EGL_TRUE ? EGL_TRUE : EGL_FALSE;
}
MOBILEGL_EGL_API EGLint eglDupNativeFenceFDANDROID(EGLDisplay dpy, EGLSyncKHR sync) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglDupNativeFenceFDANDROID(dpy=%p, sync=%p)", dpy, sync);
return MobileGL::MG_Impl::EGLImpl::DupNativeFenceFD(dpy, sync);
}
MOBILEGL_EGL_API EGLImage eglCreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attrib_list) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
@@ -384,8 +445,8 @@ MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurface(EGLDisplay dpy, EGLCo
return MobileGL::MG_Impl::EGLImpl::CreatePlatformPixmapSurface(dpy, config, native_pixmap, attrib_list);
}
// No stream lock, like eglClientWaitSync's: a native fence's server wait blocks the calling thread.
MOBILEGL_EGL_API EGLBoolean eglWaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglWaitSync(dpy=%p, sync=%p, flags=%d)", dpy, sync, flags);
return MobileGL::MG_Impl::EGLImpl::WaitSync(dpy, sync, flags);
}
@@ -44,6 +44,12 @@ extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface dra
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface draw, const EGLint* rects,
EGLint n_rects);
extern "C" EGLBoolean eglSetDamageRegionKHR(EGLDisplay dpy, EGLSurface surface, EGLint* rects, EGLint n_rects);
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
extern "C" EGLint eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
extern "C" EGLint eglDupNativeFenceFDANDROID(EGLDisplay dpy, EGLSyncKHR sync);
extern "C" EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay dpy, EGLint max_formats, EGLint* formats,
EGLint* num_formats);
extern "C" EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format, EGLint max_modifiers,
@@ -264,6 +270,12 @@ namespace {
{"eglClientWaitSync", reinterpret_cast<void*>(eglClientWaitSync)},
{"eglGetSyncAttrib", reinterpret_cast<void*>(eglGetSyncAttrib)},
{"eglWaitSync", reinterpret_cast<void*>(eglWaitSync)},
{"eglCreateSyncKHR", reinterpret_cast<void*>(eglCreateSyncKHR)},
{"eglDestroySyncKHR", reinterpret_cast<void*>(eglDestroySyncKHR)},
{"eglClientWaitSyncKHR", reinterpret_cast<void*>(eglClientWaitSyncKHR)},
{"eglGetSyncAttribKHR", reinterpret_cast<void*>(eglGetSyncAttribKHR)},
{"eglWaitSyncKHR", reinterpret_cast<void*>(eglWaitSyncKHR)},
{"eglDupNativeFenceFDANDROID", reinterpret_cast<void*>(eglDupNativeFenceFDANDROID)},
{"eglCreateImage", reinterpret_cast<void*>(eglCreateImage)},
{"eglDestroyImage", reinterpret_cast<void*>(eglDestroyImage)},
{"eglCreateImageKHR", reinterpret_cast<void*>(eglCreateImageKHR)},
+13
View File
@@ -29,6 +29,13 @@ extern "C" EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format,
khronos_uint64_t* modifiers, EGLBoolean* external_only,
EGLint* num_modifiers);
extern "C" void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image);
// EGL_KHR_fence_sync / EGL_KHR_wait_sync / EGL_ANDROID_native_fence_sync (Definitions.cpp).
extern "C" EGLSync eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSync sync);
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLint* value);
extern "C" EGLint eglWaitSyncKHR(EGLDisplay dpy, EGLSync sync, EGLint flags);
extern "C" EGLint eglDupNativeFenceFDANDROID(EGLDisplay dpy, EGLSync sync);
// KHR_robustness / EXT_robustness suffixed names (Definitions.cpp); the desktop headers declare
// only the unsuffixed and ARB ones.
extern "C" GLenum glGetGraphicsResetStatusKHR(void);
@@ -89,6 +96,12 @@ namespace MobileGL::MG_Impl {
GETPROC(eglDestroySync, name);
GETPROC(eglClientWaitSync, name);
GETPROC(eglGetSyncAttrib, name);
GETPROC(eglCreateSyncKHR, name);
GETPROC(eglDestroySyncKHR, name);
GETPROC(eglClientWaitSyncKHR, name);
GETPROC(eglGetSyncAttribKHR, name);
GETPROC(eglWaitSyncKHR, name);
GETPROC(eglDupNativeFenceFDANDROID, name);
GETPROC(eglCreateImage, name);
GETPROC(eglDestroyImage, name);
GETPROC(eglCreateImageKHR, name);
+9
View File
@@ -2016,6 +2016,13 @@ namespace MobileGL::MG_Pipe {
// Flush glFlush/glFinish of a session that renders INTO images (an X server's glamor):
// every image the session used since its last boundary is published as written
// and read by the flush's fence (implicit sync); answered once published.
// NativeFence EGL_ANDROID_native_fence_sync's fence command: a sync_file that signals
// once everything the session submitted so far has completed (and, in a session
// that writes images, what Flush does)
// reaches the client on the aux socket BEFORE the reply is posted, with the reply's
// Format = kMGPNativeFenceFdFollows. Format = 0: the server has no descriptor to give
// (it cannot export fences, and none it gave before can stand in), and the work has
// completed by the time the reply is posted.
//
// A Present carries the region of the frame to copy (DamageCount rectangles of Damage, GL
// window coordinates, origin bottom-left; 0 = all of it): the client keeps per image what the
@@ -2032,8 +2039,10 @@ namespace MobileGL::MG_Pipe {
kMGPSharedImageQueryBufferAge = 6,
kMGPSharedImageSetDamageRegion = 7,
kMGPSharedImageFlush = 8,
kMGPSharedImageNativeFence = 9,
};
inline constexpr Uint32 kMGPBufferAgeDamageRegionFollows = 1u;
inline constexpr Uint32 kMGPNativeFenceFdFollows = 1u;
struct MGPSharedImageOp {
Uint32 Op;
@@ -707,6 +707,19 @@ namespace MobileGL::MG_Remote::Client {
return published;
}
Bool BackendObject_Remote::CreateNativeFence(int* fence) {
*fence = -1;
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageNativeFence;
MG_Pipe::MGPSharedImageReply reply{};
if (!session->EmitSharedImage(op, -1, &reply, fence)) return false;
// It published what a flush would: a flush right after it has nothing new to say.
m_sharedImageFlushedAt = session->LastPublishedSeq();
return true;
}
// NO strong CreateRemoteBackendObject() lives here, and the reason is a link fact, not an
// oversight. v1's Init.cpp calls MG_Remote::Client::CreateRemoteBackendObject() and ships a
// __attribute__((weak)) placeholder for it beside ServerLoop that aborts by name; its comment
@@ -96,6 +96,7 @@ namespace MobileGL::MG_Remote::Client {
Bool SetDamageRegion(const MG_Util::Damage::Region& region) override;
Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) override;
Bool FlushSharedImageAccesses() override;
Bool CreateNativeFence(int* fence) override;
// Copies the caps mirror's FormatCapabilityCache into the base class's
// m_formatCapabilities. Public because CapsMirror's adoption hook is a free function
@@ -3067,6 +3067,9 @@ namespace MobileGL::MG_Remote::Client {
if (status != Wire::ReplySink::kStatusOk || returned != sizeof(answer)) return false;
if (reply != nullptr) *reply = answer;
if (receivedFd == nullptr) return true;
// A native fence's answer says whether a descriptor follows (none: the work completed).
if (op.Op == MG_Pipe::kMGPSharedImageNativeFence && answer.Format != MG_Pipe::kMGPNativeFenceFdFollows)
return true;
#if defined(_WIN32)
return false;
#else
+46
View File
@@ -1480,6 +1480,20 @@ namespace MobileGL::MG_Remote::Server {
return true;
}
ServerVerbSink::OwnedFd& ServerVerbSink::OwnedFd::operator=(OwnedFd&& other) noexcept {
if (this != &other) Reset(std::exchange(other.fd, -1));
return *this;
}
ServerVerbSink::OwnedFd::~OwnedFd() { Reset(-1); }
void ServerVerbSink::OwnedFd::Reset(int next) {
#if !defined(_WIN32)
if (fd >= 0) ::close(fd);
#endif
fd = next;
}
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
//
// The record carries the client's current context token and this is where a session's
@@ -1593,6 +1607,38 @@ namespace MobileGL::MG_Remote::Server {
if (backend == nullptr) return false;
return backend->PublishSharedImageAccesses();
}
case MG_Pipe::kMGPSharedImageNativeFence: {
// A descriptor of the session's work, and an image writer's flush (MGPipeTypes.h). Like
// an Allocate's, the descriptor goes out BEFORE the reply is posted.
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
if (backend == nullptr) return false;
int fence = -1;
if (!backend->ExportNativeFence(&fence)) return false;
#if !defined(_WIN32)
if (fence >= 0) {
m_lastNativeFence.Reset(::dup(fence));
} else if (m_lastNativeFence.fd >= 0) {
fence = ::dup(m_lastNativeFence.fd);
}
#endif
reply.Format = 0;
if (fence < 0) return true;
Transport::ITransport* transport = session->ControlTransport();
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, 0, 0};
const MobileGLResult shared = transport != nullptr
? transport->ShareFd(fence, MobileGLByteSpan{&offer, sizeof(offer)})
: MOBILEGL_ERR_NOT_INITIALIZED;
#if !defined(_WIN32)
::close(fence);
#endif
if (shared != MOBILEGL_OK) {
MGLOG_E_ONCE("MG_Remote server: a native fence could not be sent on the aux socket (rc=%d)",
static_cast<int>(shared));
return false;
}
reply.Format = MG_Pipe::kMGPNativeFenceFdFollows;
return true;
}
case MG_Pipe::kMGPSharedImageAttach: {
SI::ImageRef image = holder.Get(op.ImageId);
if (image == nullptr) return false;
+15
View File
@@ -47,6 +47,7 @@
#include "../Wire/PipeWireCodec.h"
#include <deque>
#include <utility>
namespace MobileGL::MG_Remote::Server {
@@ -413,6 +414,20 @@ namespace MobileGL::MG_Remote::Server {
Vector<GLint> m_multiBaseVertices;
LastDrawRecord m_lastDraw{};
Uint64 m_drawRecords = 0;
// The newest native fence (shared_image NativeFence) this session handed out, kept so a
// later fence with nothing left to export still answers with a descriptor: by then this
// one has signaled (the backend waited the work out), and a signaled sync_file stays so.
struct OwnedFd {
int fd = -1;
OwnedFd() = default;
OwnedFd(const OwnedFd&) = delete;
OwnedFd& operator=(const OwnedFd&) = delete;
OwnedFd(OwnedFd&& other) noexcept : fd(std::exchange(other.fd, -1)) {}
OwnedFd& operator=(OwnedFd&& other) noexcept;
~OwnedFd();
void Reset(int next);
};
OwnedFd m_lastNativeFence;
};
class PipeApplier {
+150 -16
View File
@@ -10,6 +10,13 @@
#include <EGL/eglext.h>
#include <MG_Util/X11/DisplayGuard.h>
#include <cerrno>
#include <climits>
#if !defined(_WIN32)
#include <poll.h>
#include <unistd.h>
#endif
namespace MobileGL {
namespace MG_State {
namespace EGLState {
@@ -223,6 +230,55 @@ namespace MobileGL {
return it == m_surfaces.end() ? nullptr : &it->second;
}
namespace {
void CloseNativeFd(int fd) {
#if !defined(_WIN32)
if (fd >= 0) ::close(fd);
#else
(void)fd;
#endif
}
// Waits up to `timeoutMs` (negative: for ever) for a sync_file to signal; an absent
// descriptor has. False on timeout.
Bool WaitNativeFd(int fd, int timeoutMs) {
#if !defined(_WIN32)
if (fd < 0) return true;
pollfd entry{fd, POLLIN, 0};
for (;;) {
const int ready = ::poll(&entry, 1, timeoutMs);
if (ready > 0) return true;
if (ready == 0) return false;
if (errno != EINTR && errno != EAGAIN) return true; // not a fence that can block
}
#else
(void)fd, (void)timeoutMs;
return true;
#endif
}
int DupNativeFd(int fd) {
#if !defined(_WIN32)
return fd >= 0 ? ::dup(fd) : -1;
#else
(void)fd;
return -1;
#endif
}
// EGLTimeKHR nanoseconds as a poll timeout, rounded up; EGL_FOREVER waits for ever.
int NativeWaitTimeoutMs(EGLTime timeout) {
if (timeout == EGL_FOREVER) return -1;
const EGLTime ms = timeout / 1000000u + (timeout % 1000000u != 0 ? 1u : 0u);
return ms > static_cast<EGLTime>(INT_MAX) ? INT_MAX : static_cast<int>(ms);
}
// A server wait on a native fence is a wait of the calling thread: nothing it issues
// later reaches the server before the fence signals. Bounded, so a fence that never
// signals costs a stall rather than the application.
constexpr int kNativeServerWaitMs = 2000;
} // namespace
const EGLContext::SyncObject* EGLContext::TryGetSync(EGLSyncHandle sync) const {
auto it = m_syncs.find(sync);
return it == m_syncs.end() ? nullptr : &it->second;
@@ -334,6 +390,7 @@ namespace MobileGL {
for (auto syncIt = m_syncs.begin(); syncIt != m_syncs.end();) {
if (syncIt->second.Display == display) {
CloseNativeFd(syncIt->second.NativeFd);
syncIt = m_syncs.erase(syncIt);
} else {
++syncIt;
@@ -1595,6 +1652,19 @@ namespace MobileGL {
SetError(EGL_NOT_INITIALIZED);
return EGL_NO_SYNC;
}
if (type == EGL_SYNC_NATIVE_FENCE_ANDROID) {
// An application's descriptor to wait on. A fence command (no descriptor) needs
// the backend, which EGLImpl asks before it gets here (CreateNativeFenceSync).
const auto fd = ParseAttribValue(attribList, EGL_SYNC_NATIVE_FENCE_FD_ANDROID);
#if !defined(_WIN32)
if (fd && *fd != EGL_NO_NATIVE_FENCE_FD_ANDROID) {
return CreateNativeFenceSync(display, static_cast<int>(*fd),
EGL_SYNC_NATIVE_FENCE_SIGNALED_ANDROID);
}
#endif
SetError(EGL_BAD_ATTRIBUTE);
return EGL_NO_SYNC;
}
if (type != EGL_SYNC_FENCE) {
SetError(EGL_BAD_ATTRIBUTE);
return EGL_NO_SYNC;
@@ -1626,27 +1696,80 @@ namespace MobileGL {
SetError(EGL_BAD_MATCH);
return false;
}
CloseNativeFd(syncIt->second.NativeFd);
m_syncs.erase(syncIt);
return true;
}
EGLint EGLContext::ClientWaitSync(EGLDisplayHandle display, EGLSyncHandle sync, EGLint flags,
EGLTime timeout) {
EGLContext::EGLSyncHandle EGLContext::CreateNativeFenceSync(EGLDisplayHandle display, int fd,
EGLenum condition) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
(void)flags;
(void)timeout;
#if defined(_WIN32)
(void)display, (void)fd, (void)condition;
SetError(EGL_BAD_ATTRIBUTE);
return EGL_NO_SYNC;
#else
if (!IsDisplayInitialized(display)) {
CloseNativeFd(fd);
SetError(EGL_NOT_INITIALIZED);
return EGL_NO_SYNC;
}
SyncObject sync = {
.Display = display,
.Type = EGL_SYNC_NATIVE_FENCE_ANDROID,
.Condition = condition,
.Status = EGL_UNSIGNALED,
.NativeFd = fd,
};
const auto syncHandle = EncodeHandle<EGLSyncHandle>(m_nextSyncHandle++);
m_syncs[syncHandle] = sync;
return syncHandle;
#endif
}
EGLint EGLContext::DupNativeFenceFD(EGLDisplayHandle display, EGLSyncHandle sync) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* syncObject = TryGetSync(sync);
if (!syncObject) {
SetError(EGL_BAD_PARAMETER);
return EGL_FALSE;
return EGL_NO_NATIVE_FENCE_FD_ANDROID;
}
if (syncObject->Display != display) {
SetError(EGL_BAD_MATCH);
return EGL_FALSE;
return EGL_NO_NATIVE_FENCE_FD_ANDROID;
}
const int fd =
syncObject->Type == EGL_SYNC_NATIVE_FENCE_ANDROID ? DupNativeFd(syncObject->NativeFd) : -1;
if (fd < 0) {
SetError(EGL_BAD_PARAMETER);
return EGL_NO_NATIVE_FENCE_FD_ANDROID;
}
return fd;
}
return EGL_CONDITION_SATISFIED;
EGLint EGLContext::ClientWaitSync(EGLDisplayHandle display, EGLSyncHandle sync, EGLint flags,
EGLTime timeout) {
(void)flags;
int fd = -1;
{
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* syncObject = TryGetSync(sync);
if (!syncObject) {
SetError(EGL_BAD_PARAMETER);
return EGL_FALSE;
}
if (syncObject->Display != display) {
SetError(EGL_BAD_MATCH);
return EGL_FALSE;
}
if (syncObject->Type != EGL_SYNC_NATIVE_FENCE_ANDROID) return EGL_CONDITION_SATISFIED;
// A copy, waited on outside the lock: other threads keep using EGL meanwhile,
// and one may destroy the sync while this waits.
fd = DupNativeFd(syncObject->NativeFd);
}
const Bool signaled = WaitNativeFd(fd, NativeWaitTimeoutMs(timeout));
CloseNativeFd(fd);
return signaled ? EGL_CONDITION_SATISFIED : EGL_TIMEOUT_EXPIRED;
}
Bool EGLContext::GetSyncAttrib(EGLDisplayHandle display, EGLSyncHandle sync, EGLint attribute,
@@ -1671,6 +1794,10 @@ namespace MobileGL {
*value = static_cast<EGLAttrib>(syncObject->Type);
return true;
case EGL_SYNC_STATUS:
if (syncObject->Type == EGL_SYNC_NATIVE_FENCE_ANDROID) {
*value = WaitNativeFd(syncObject->NativeFd, 0) ? EGL_SIGNALED : EGL_UNSIGNALED;
return true;
}
*value = static_cast<EGLAttrib>(syncObject->Status);
return true;
case EGL_SYNC_CONDITION:
@@ -1683,17 +1810,24 @@ namespace MobileGL {
}
Bool EGLContext::WaitSync(EGLDisplayHandle display, EGLSyncHandle sync, EGLint flags) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
(void)flags;
const auto* syncObject = TryGetSync(sync);
if (!syncObject) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_PARAMETER);
return false;
}
if (syncObject->Display != display) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_MATCH);
return false;
int fd = -1;
{
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* syncObject = TryGetSync(sync);
if (!syncObject) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_PARAMETER);
return false;
}
if (syncObject->Display != display) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_MATCH);
return false;
}
if (syncObject->Type != EGL_SYNC_NATIVE_FENCE_ANDROID) return true;
fd = DupNativeFd(syncObject->NativeFd);
}
(void)WaitNativeFd(fd, kNativeServerWaitMs);
CloseNativeFd(fd);
return true;
}
+11
View File
@@ -168,6 +168,15 @@ namespace MobileGL {
Bool GetSyncAttrib(EGLDisplayHandle display, EGLSyncHandle sync, EGLint attribute,
EGLAttrib* value) const;
Bool WaitSync(EGLDisplayHandle display, EGLSyncHandle sync, EGLint flags) const;
// EGL_ANDROID_native_fence_sync. A native fence sync owns a sync_file descriptor and
// its status is that descriptor's: CreateSync takes one an application imports
// (EGL_SYNC_NATIVE_FENCE_FD_ANDROID), and CreateNativeFenceSync takes the one a fence
// command produced (`condition` EGL_SYNC_PRIOR_COMMANDS_COMPLETE; fd -1 = the fenced
// work has completed and no descriptor came with it). Both take ownership of `fd`,
// also on failure. DupNativeFenceFD hands out a copy, or EGL_NO_NATIVE_FENCE_FD_ANDROID
// (EGL_BAD_PARAMETER) when the sync has none. Not on Windows, which has no sync_file.
EGLSyncHandle CreateNativeFenceSync(EGLDisplayHandle display, int fd, EGLenum condition);
EGLint DupNativeFenceFD(EGLDisplayHandle display, EGLSyncHandle sync);
// Image
EGLImageHandle CreateImage(EGLDisplayHandle display, EGLContextHandle context, EGLenum target,
@@ -335,6 +344,8 @@ namespace MobileGL {
EGLenum Type = EGL_SYNC_FENCE;
EGLenum Condition = EGL_SYNC_PRIOR_COMMANDS_COMPLETE;
EGLenum Status = EGL_SIGNALED;
// EGL_SYNC_NATIVE_FENCE_ANDROID only: the owned sync_file, -1 = none (signaled).
int NativeFd = -1;
};
struct ImageObject {
@@ -527,3 +527,79 @@ TEST(EGLStateRobustness, RobustAccessIsARequestAndNeverAGLContextFlagByItself) {
const EGLint off[] = {EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, EGL_FALSE, EGL_NONE};
EXPECT_FALSE(requestOf(off, &flags));
}
#if !defined(_WIN32)
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
// EGL_ANDROID_native_fence_sync, the half that is pure state: a native fence sync owns a sync_file
// and answers its status, waits and copies from it. A pipe's read end stands in for the sync_file -
// it polls readable once something is written, as a fence does once it signals.
TEST(EGLStateNativeFence, AnImportedDescriptorDecidesStatusWaitsAndCopies) {
auto fixture = CreateFixture();
int fence[2] = {-1, -1};
ASSERT_EQ(::pipe(fence), 0);
const EGLAttrib attribs[] = {EGL_SYNC_NATIVE_FENCE_FD_ANDROID, fence[0], EGL_NONE};
const EGLSync sync = fixture->State.CreateSync(fixture->Display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
ASSERT_NE(sync, EGL_NO_SYNC) << "error 0x" << std::hex << fixture->State.ConsumeError();
EGLAttrib value = 0;
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_TYPE, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_SYNC_NATIVE_FENCE_ANDROID));
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_CONDITION, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_SYNC_NATIVE_FENCE_SIGNALED_ANDROID));
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_STATUS, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_UNSIGNALED));
EXPECT_EQ(fixture->State.ClientWaitSync(fixture->Display, sync, 0, 0), EGL_TIMEOUT_EXPIRED);
EXPECT_EQ(fixture->State.ClientWaitSync(fixture->Display, sync, 0, 5u * 1000 * 1000), EGL_TIMEOUT_EXPIRED);
// A copy is a descriptor of the same fence: it signals with it.
const EGLint copy = fixture->State.DupNativeFenceFD(fixture->Display, sync);
ASSERT_GE(copy, 0);
EXPECT_NE(copy, fence[0]);
ASSERT_EQ(::write(fence[1], "s", 1), 1);
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_STATUS, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_SIGNALED));
EXPECT_EQ(fixture->State.ClientWaitSync(fixture->Display, sync, 0, EGL_FOREVER), EGL_CONDITION_SATISFIED);
EXPECT_TRUE(fixture->State.WaitSync(fixture->Display, sync, 0));
pollfd entry{copy, POLLIN, 0};
EXPECT_EQ(::poll(&entry, 1, 0), 1);
// The sync owned its descriptor: destroying it closes that, and the copy stays the caller's.
EXPECT_TRUE(fixture->State.DestroySync(fixture->Display, sync));
EXPECT_EQ(::fcntl(fence[0], F_GETFD), -1);
EXPECT_NE(::fcntl(copy, F_GETFD), -1);
::close(copy);
::close(fence[1]);
}
// A fence command whose work completed with no descriptor to show for it is signaled, and has no
// descriptor to copy; a native fence without one is no import either, and an ordinary fence has
// none to copy.
TEST(EGLStateNativeFence, AFenceCommandWithoutADescriptorIsSignaledAndHasNoneToCopy) {
auto fixture = CreateFixture();
const EGLSync done = fixture->State.CreateNativeFenceSync(fixture->Display, -1, EGL_SYNC_PRIOR_COMMANDS_COMPLETE);
ASSERT_NE(done, EGL_NO_SYNC);
EGLAttrib value = 0;
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, done, EGL_SYNC_STATUS, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_SIGNALED));
ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, done, EGL_SYNC_CONDITION, &value));
EXPECT_EQ(value, static_cast<EGLAttrib>(EGL_SYNC_PRIOR_COMMANDS_COMPLETE));
EXPECT_EQ(fixture->State.ClientWaitSync(fixture->Display, done, 0, 0), EGL_CONDITION_SATISFIED);
EXPECT_EQ(fixture->State.DupNativeFenceFD(fixture->Display, done), EGL_NO_NATIVE_FENCE_FD_ANDROID);
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_PARAMETER);
EXPECT_TRUE(fixture->State.DestroySync(fixture->Display, done));
const EGLAttrib none[] = {EGL_NONE};
EXPECT_EQ(fixture->State.CreateSync(fixture->Display, EGL_SYNC_NATIVE_FENCE_ANDROID, none), EGL_NO_SYNC);
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_ATTRIBUTE);
const EGLSync plain = fixture->State.CreateSync(fixture->Display, EGL_SYNC_FENCE, none);
ASSERT_NE(plain, EGL_NO_SYNC);
EXPECT_EQ(fixture->State.DupNativeFenceFD(fixture->Display, plain), EGL_NO_NATIVE_FENCE_FD_ANDROID);
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_PARAMETER);
EXPECT_TRUE(fixture->State.DestroySync(fixture->Display, plain));
}
#endif
+206 -6
View File
@@ -19,6 +19,13 @@
//
// And the implicit-sync flush (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the shared_image
// Flush verb the client sends from glFlush is answered by the server.
//
// Chrome's GPU process on the same kind of display (a GBM one) is the other dma-buf producer, and it
// never flushes: when the compositor's kernel takes dma-buf fences it ends each frame by creating an
// EGL_ANDROID_native_fence_sync fence (through ANGLE, which offers fences only when the native EGL
// has EGL_KHR_fence_sync and the extension, and loads the KHR entry points by name) and hands its
// descriptor to the browser with the buffer. That fence is the only point at which the frame's
// writes into the buffer can reach the compositor's session, so it must be a real one.
#include "P12ServerRig.h"
@@ -173,6 +180,158 @@ namespace {
(void)EGL::Terminate(dpy);
}
struct ChromeFenceReport {
Int32 initialized = 0;
Int32 fenceSyncExtension = 0;
Int32 waitSyncExtension = 0;
Int32 nativeFenceExtension = 0;
Int32 entryPoints = 0;
Int32 current = 0;
Int32 imageBound = 0;
Int32 framebufferComplete = 0;
Int32 noContextRefused = 0;
Int32 fenceCreated = 0;
Int32 fenceType = 0;
Int32 fenceWaited = 0;
Int32 fenceSignaled = 0;
Int32 dupFd = -2;
Int32 dupError = 0;
Int32 dupSignaled = 0;
Int32 importCreated = 0;
Int32 importWaited = 0;
Int32 eglError = 0;
char note[128] = {};
};
// ANGLE's FunctionsEGL and Chrome's GLFenceAndroidNativeFenceSync / GbmSurfacelessWayland,
// reduced to the calls they make on the native EGL.
void RunChromeGpuFence(ChromeFenceReport& r) {
using CreateSyncFn = EGLSyncKHR (*)(EGLDisplay, EGLenum, const EGLint*);
using DestroySyncFn = EGLBoolean (*)(EGLDisplay, EGLSyncKHR);
using ClientWaitFn = EGLint (*)(EGLDisplay, EGLSyncKHR, EGLint, EGLTimeKHR);
using GetSyncAttribFn = EGLBoolean (*)(EGLDisplay, EGLSyncKHR, EGLint, EGLint*);
using WaitSyncFn = EGLint (*)(EGLDisplay, EGLSyncKHR, EGLint);
using DupFn = EGLint (*)(EGLDisplay, EGLSyncKHR);
static int fakeGbmDevice[16] = {};
const EGLDisplay dpy = EGL::GetPlatformDisplay(kPlatformGbm, fakeGbmDevice, nullptr);
if (dpy == EGL_NO_DISPLAY) return;
EGLint major = 0, minor = 0;
r.initialized = EGL::Initialize(dpy, &major, &minor) == EGL_TRUE ? 1 : 0;
if (!r.initialized) return;
const char* exts = EGL::QueryString(dpy, EGL_EXTENSIONS);
r.fenceSyncExtension = HasToken(exts, "EGL_KHR_fence_sync") ? 1 : 0;
r.waitSyncExtension = HasToken(exts, "EGL_KHR_wait_sync") ? 1 : 0;
r.nativeFenceExtension = HasToken(exts, "EGL_ANDROID_native_fence_sync") ? 1 : 0;
const auto createSync = reinterpret_cast<CreateSyncFn>(EGL::GetProcAddress("eglCreateSyncKHR"));
const auto destroySync = reinterpret_cast<DestroySyncFn>(EGL::GetProcAddress("eglDestroySyncKHR"));
const auto clientWait = reinterpret_cast<ClientWaitFn>(EGL::GetProcAddress("eglClientWaitSyncKHR"));
const auto getSyncAttrib = reinterpret_cast<GetSyncAttribFn>(EGL::GetProcAddress("eglGetSyncAttribKHR"));
const auto waitSync = reinterpret_cast<WaitSyncFn>(EGL::GetProcAddress("eglWaitSyncKHR"));
const auto dupFence = reinterpret_cast<DupFn>(EGL::GetProcAddress("eglDupNativeFenceFDANDROID"));
r.entryPoints = createSync && destroySync && clientWait && getSyncAttrib && waitSync && dupFence ? 1 : 0;
if (!r.entryPoints) return;
// A fence command needs a current context.
r.noContextRefused = createSync(dpy, EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr) == EGL_NO_SYNC_KHR &&
EGL::GetError() == EGL_BAD_MATCH
? 1
: 0;
// Chrome's GPU process: ES through ANGLE's identity, surfaceless, no config.
(void)EGL::BindAPI(EGL_OPENGL_ES_API);
const EGLint es3[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
const EGLContext ctx = EGL::CreateContext(dpy, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, es3);
if (ctx == EGL_NO_CONTEXT) {
r.eglError = EGL::GetError();
return;
}
r.current = EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) == EGL_TRUE ? 1 : 0;
if (!r.current) {
r.eglError = EGL::GetError();
return;
}
// A GBM scanout buffer (a shared image), bound as a texture and rendered into through a
// framebuffer - and then no glFlush, as Chrome does when it has native fences.
auto* backend = MG_Backend::pActiveBackendObject.get();
MG_Backend::SharedImageExport image;
EGLImage eglImage = EGL_NO_IMAGE;
using GenFn = void (*)(int, unsigned*);
using BindFn = void (*)(unsigned, unsigned);
using ImageTargetFn = void (*)(unsigned, void*);
using FbTexFn = void (*)(unsigned, unsigned, unsigned, unsigned, int);
using StatusFn = unsigned (*)(unsigned);
using ClearColorFn = void (*)(float, float, float, float);
using ClearFn = void (*)(unsigned);
const auto genTextures = reinterpret_cast<GenFn>(EGL::GetProcAddress("glGenTextures"));
const auto bindTexture = reinterpret_cast<BindFn>(EGL::GetProcAddress("glBindTexture"));
const auto imageTarget = reinterpret_cast<ImageTargetFn>(EGL::GetProcAddress("glEGLImageTargetTexture2DOES"));
const auto genFramebuffers = reinterpret_cast<GenFn>(EGL::GetProcAddress("glGenFramebuffers"));
const auto bindFramebuffer = reinterpret_cast<BindFn>(EGL::GetProcAddress("glBindFramebuffer"));
const auto framebufferTexture = reinterpret_cast<FbTexFn>(EGL::GetProcAddress("glFramebufferTexture2D"));
const auto checkStatus = reinterpret_cast<StatusFn>(EGL::GetProcAddress("glCheckFramebufferStatus"));
const auto clearColor = reinterpret_cast<ClearColorFn>(EGL::GetProcAddress("glClearColor"));
const auto clear = reinterpret_cast<ClearFn>(EGL::GetProcAddress("glClear"));
if (backend != nullptr && backend->AllocateSharedImage(64, 32, kArgb8888, &image) && image.Fd >= 0) {
const EGLAttrib attribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 32, EGL_LINUX_DRM_FOURCC_EXT, kArgb8888,
EGL_DMA_BUF_PLANE0_FD_EXT, image.Fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT,
static_cast<EGLAttrib>(image.Offset), EGL_DMA_BUF_PLANE0_PITCH_EXT,
static_cast<EGLAttrib>(image.Stride), EGL_NONE};
eglImage = EGL::CreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs);
}
if (eglImage != EGL_NO_IMAGE && genTextures && bindTexture && imageTarget && genFramebuffers &&
bindFramebuffer && framebufferTexture && checkStatus && clearColor && clear) {
unsigned texture = 0, framebuffer = 0;
genTextures(1, &texture);
bindTexture(0x0DE1 /* GL_TEXTURE_2D */, texture);
imageTarget(0x0DE1, eglImage);
r.imageBound = 1;
genFramebuffers(1, &framebuffer);
bindFramebuffer(0x8D40 /* GL_FRAMEBUFFER */, framebuffer);
framebufferTexture(0x8D40, 0x8CE0 /* GL_COLOR_ATTACHMENT0 */, 0x0DE1, texture, 0);
r.framebufferComplete = checkStatus(0x8D40) == 0x8CD5 /* GL_FRAMEBUFFER_COMPLETE */ ? 1 : 0;
clearColor(0.0f, 1.0f, 1.0f, 1.0f);
clear(0x4000);
}
// GLFenceAndroidNativeFenceSync::CreateForGpuFence, then GetGpuFenceHandle.
const EGLSyncKHR fence = createSync(dpy, EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr);
r.fenceCreated = fence != EGL_NO_SYNC_KHR ? 1 : 0;
if (!r.fenceCreated) {
r.eglError = EGL::GetError();
} else {
EGLint value = 0;
if (getSyncAttrib(dpy, fence, EGL_SYNC_TYPE_KHR, &value)) r.fenceType = value;
r.fenceWaited = clientWait(dpy, fence, 0, EGL_FOREVER_KHR) == EGL_CONDITION_SATISFIED_KHR ? 1 : 0;
if (getSyncAttrib(dpy, fence, EGL_SYNC_STATUS_KHR, &value)) r.fenceSignaled = value == EGL_SIGNALED_KHR;
r.dupFd = dupFence(dpy, fence);
r.dupError = r.dupFd < 0 ? EGL::GetError() : EGL_SUCCESS;
if (r.dupFd >= 0) {
pollfd entry{r.dupFd, POLLIN, 0};
r.dupSignaled = ::poll(&entry, 1, 5000) == 1 ? 1 : 0;
// GLFenceAndroidNativeFenceSync::CreateFromGpuFenceHandle + ServerWait: the
// descriptor imported back (the sync takes it) and waited on.
const EGLint importAttribs[] = {EGL_SYNC_NATIVE_FENCE_FD_ANDROID, r.dupFd, EGL_NONE};
const EGLSyncKHR imported = createSync(dpy, EGL_SYNC_NATIVE_FENCE_ANDROID, importAttribs);
r.importCreated = imported != EGL_NO_SYNC_KHR ? 1 : 0;
if (imported != EGL_NO_SYNC_KHR) {
r.importWaited = waitSync(dpy, imported, 0) == EGL_TRUE ? 1 : 0;
(void)destroySync(dpy, imported);
} else {
::close(r.dupFd);
}
}
(void)destroySync(dpy, fence);
}
if (eglImage != EGL_NO_IMAGE) (void)EGL::DestroyImage(dpy, eglImage);
if (image.Fd >= 0) ::close(image.Fd);
if (backend != nullptr && image.Id != 0) (void)backend->ReleaseSharedImage(image.Id);
(void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(dpy, ctx);
(void)EGL::Terminate(dpy);
}
// The supervisor on an abstract unix socket, as the device's is: shared images travel as
// descriptors on its aux socket, which a TCP endpoint does not have.
bool LaunchUnixSupervisor(ServerProcess* server) {
@@ -190,8 +349,10 @@ namespace {
return WaitFor([&] { return server->Log().find("listening on") != std::string::npos; }, 10000);
}
GlamorReport RunGlamorPeer(const ServerProcess& server, const std::string& clientLogBase) {
GlamorReport report{};
// One forked client running `run` against the server, its report back through a pipe.
template <class Report>
Report RunPeer(const ServerProcess& server, const std::string& clientLogBase, void (*run)(Report&)) {
Report report{};
int fds[2] = {-1, -1};
if (::pipe(fds) != 0) return report;
std::fflush(nullptr);
@@ -203,8 +364,8 @@ namespace {
::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1);
::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
::setenv("MOBILEGL_IPC_SURFACE", "offscreen", 1);
GlamorReport r{};
RunGlamor(r);
Report r{};
run(r);
const ssize_t wrote = ::write(fds[1], &r, sizeof(r));
(void)wrote;
std::fflush(nullptr);
@@ -218,7 +379,7 @@ namespace {
pollfd pfd{fds[0], POLLIN, 0};
if (::poll(&pfd, 1, 90000) > 0) {
if (::read(fds[0], &report, sizeof(report)) != static_cast<ssize_t>(sizeof(report))) {
report = GlamorReport{};
report = Report{};
std::snprintf(report.note, sizeof(report.note), "peer died before reporting");
}
} else {
@@ -237,7 +398,7 @@ TEST(GlamorEglSequence, XwaylandFindsEverythingGlamorNeeds) {
ASSERT_TRUE(LaunchUnixSupervisor(&server)) << server.Log();
const std::string clientLog = "/tmp/mgl-glamor-client-" + std::to_string(::getpid()) + ".log";
Debug::TruncateRoleLogs(clientLog.c_str());
const GlamorReport r = RunGlamorPeer(server, clientLog);
const GlamorReport r = RunPeer<GlamorReport>(server, clientLog, RunGlamor);
ASSERT_EQ(r.display, 1) << r.note;
EXPECT_EQ(r.platformGbmExtension, 1);
ASSERT_EQ(r.initialized, 1);
@@ -263,3 +424,42 @@ TEST(GlamorEglSequence, XwaylandFindsEverythingGlamorNeeds) {
std::filesystem::remove(clientLog, ec);
}
}
TEST(GlamorEglSequence, ChromesGpuProcessGetsANativeFenceOfItsDmaBufWrites) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixSupervisor(&server)) << server.Log();
const std::string clientLog = "/tmp/mgl-chrome-fence-client-" + std::to_string(::getpid()) + ".log";
Debug::TruncateRoleLogs(clientLog.c_str());
const ChromeFenceReport r = RunPeer<ChromeFenceReport>(server, clientLog, RunChromeGpuFence);
ASSERT_EQ(r.initialized, 1) << r.note;
// ANGLE offers Chrome fences only with all three, and loads the KHR names.
EXPECT_EQ(r.fenceSyncExtension, 1);
EXPECT_EQ(r.waitSyncExtension, 1);
EXPECT_EQ(r.nativeFenceExtension, 1);
ASSERT_EQ(r.entryPoints, 1) << "eglCreateSyncKHR & co. or eglDupNativeFenceFDANDROID did not resolve";
EXPECT_EQ(r.noContextRefused, 1);
ASSERT_EQ(r.current, 1) << "error 0x" << std::hex << r.eglError;
std::printf("[ chrome ] image bound %d, framebuffer complete %d, fence descriptor %d (error 0x%x)\n",
r.imageBound, r.framebufferComplete, r.dupFd, r.dupError);
// The fence command reaches the server, which publishes the context's image writes and answers.
ASSERT_EQ(r.fenceCreated, 1) << "the native fence was refused, error 0x" << std::hex << r.eglError;
EXPECT_EQ(r.fenceType, EGL_SYNC_NATIVE_FENCE_ANDROID);
EXPECT_EQ(r.fenceWaited, 1);
EXPECT_EQ(r.fenceSignaled, 1);
if (r.dupFd >= 0) {
// A sync_file of the work, which signals; imported back it is waited on.
EXPECT_EQ(r.dupSignaled, 1);
EXPECT_EQ(r.importCreated, 1);
EXPECT_EQ(r.importWaited, 1);
} else {
// A server that cannot export fences (a host driver without them) waits the work out and
// has no descriptor: the copy is refused by name, not answered with garbage.
EXPECT_EQ(r.dupError, EGL_BAD_PARAMETER);
}
std::error_code ec;
if (!::testing::Test::HasFailure()) {
std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Client), ec);
std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Server), ec);
std::filesystem::remove(clientLog, ec);
}
}