From 4f6f4125acf7df993f5300531dd610e4337ccfbd Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 3 Oct 2026 10:32:48 -0400 Subject: [PATCH] [Fix] (EGL, SharedImage): real EGL native fences publish a dma-buf producer's writes, so Chrome's GPU process frames reach the compositor. --- MobileGL/MG_Backend/BackendObject.h | 16 ++ .../DirectGLES/BackendObject_DirectGLES.cpp | 9 + .../DirectGLES/BackendObject_DirectGLES.h | 2 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 26 +++ MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 5 + .../BackendObject_DirectVulkan.cpp | 10 + .../DirectVulkan/BackendObject_DirectVulkan.h | 2 + .../DirectVulkan/Renderer/VulkanRenderer.h | 10 +- .../DirectVulkan/Renderer/WireFramebuffer.inc | 7 +- .../DirectVulkan/Renderer/WireSharedImage.inc | 41 ++++ MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 40 ++++ MobileGL/MG_Impl/EGLImpl/EGLImpl.h | 2 + .../MG_Impl/EGLImpl/EGLPlatformExtensions.h | 6 +- .../MG_Impl/EGLImpl/Exporting/Definitions.cpp | 65 +++++- .../MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp | 12 + MobileGL/MG_Impl/GetProcAddress.cpp | 13 ++ MobileGL/MG_Pipe/MGPipeTypes.h | 9 + .../MG_Remote/Client/BackendObject_Remote.cpp | 13 ++ .../MG_Remote/Client/BackendObject_Remote.h | 1 + MobileGL/MG_Remote/Client/ClientSession.cpp | 3 + MobileGL/MG_Remote/Server/PipeApplier.cpp | 46 ++++ MobileGL/MG_Remote/Server/PipeApplier.h | 15 ++ MobileGL/MG_State/EGLState/Core.cpp | 166 ++++++++++++-- MobileGL/MG_State/EGLState/Core.h | 11 + MobileGL/MG_Test/EGLState/EGLStateTest.cpp | 76 +++++++ .../MG_Test/Wire/GlamorEglSequenceTest.cpp | 212 +++++++++++++++++- 26 files changed, 790 insertions(+), 28 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 2efc3c0a..c178386d 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 57b3a44d..9a89ead1 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -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); } diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 565ffde2..bba59102 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index c4c838ea..a0471f06 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 68892b49..e70daef9 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -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 diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 7cccee83..83e8f20b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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 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) { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 26563751..584b1cef 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -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, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 0fa6e241..4b29d86f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 6e5e645b..a2953f5e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -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), diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc index 892a5b0a..9077ab90 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index f19d00a3..9ca72f2b 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -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) { diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h index d98cbb89..71a103b4 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h @@ -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); diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index d083195a..2b776f67 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp index d062b0b7..e658486c 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp @@ -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(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(at[0]); + out[n++] = static_cast(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(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(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); } diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp index 15f24fe4..b6cf3efd 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp @@ -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(eglClientWaitSync)}, {"eglGetSyncAttrib", reinterpret_cast(eglGetSyncAttrib)}, {"eglWaitSync", reinterpret_cast(eglWaitSync)}, + {"eglCreateSyncKHR", reinterpret_cast(eglCreateSyncKHR)}, + {"eglDestroySyncKHR", reinterpret_cast(eglDestroySyncKHR)}, + {"eglClientWaitSyncKHR", reinterpret_cast(eglClientWaitSyncKHR)}, + {"eglGetSyncAttribKHR", reinterpret_cast(eglGetSyncAttribKHR)}, + {"eglWaitSyncKHR", reinterpret_cast(eglWaitSyncKHR)}, + {"eglDupNativeFenceFDANDROID", reinterpret_cast(eglDupNativeFenceFDANDROID)}, {"eglCreateImage", reinterpret_cast(eglCreateImage)}, {"eglDestroyImage", reinterpret_cast(eglDestroyImage)}, {"eglCreateImageKHR", reinterpret_cast(eglCreateImageKHR)}, diff --git a/MobileGL/MG_Impl/GetProcAddress.cpp b/MobileGL/MG_Impl/GetProcAddress.cpp index b0635067..c7bd03b7 100644 --- a/MobileGL/MG_Impl/GetProcAddress.cpp +++ b/MobileGL/MG_Impl/GetProcAddress.cpp @@ -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); diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index fe9a3d78..24df2d4b 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -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; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index 989601ee..358f6af1 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -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 diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index a95a914e..7cbae559 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index e0dd62f5..6aeb1b3c 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -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 diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 790e9e57..f94f41fa 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -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(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; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 07564454..1536aa6a 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -47,6 +47,7 @@ #include "../Wire/PipeWireCodec.h" #include +#include namespace MobileGL::MG_Remote::Server { @@ -413,6 +414,20 @@ namespace MobileGL::MG_Remote::Server { Vector 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 { diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index 15f5eb20..3800226a 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -10,6 +10,13 @@ #include #include +#include +#include +#if !defined(_WIN32) +#include +#include +#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(INT_MAX) ? INT_MAX : static_cast(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(*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 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(m_nextSyncHandle++); + m_syncs[syncHandle] = sync; + return syncHandle; +#endif + } + EGLint EGLContext::DupNativeFenceFD(EGLDisplayHandle display, EGLSyncHandle sync) { + const std::lock_guard 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 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(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(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 lock(m_mutex); (void)flags; - const auto* syncObject = TryGetSync(sync); - if (!syncObject) { - const_cast(this)->SetError(EGL_BAD_PARAMETER); - return false; - } - if (syncObject->Display != display) { - const_cast(this)->SetError(EGL_BAD_MATCH); - return false; + int fd = -1; + { + const std::lock_guard lock(m_mutex); + const auto* syncObject = TryGetSync(sync); + if (!syncObject) { + const_cast(this)->SetError(EGL_BAD_PARAMETER); + return false; + } + if (syncObject->Display != display) { + const_cast(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; } diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index 368bd39a..68c376af 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -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 { diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index c1e68ccb..1d62fdba 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -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 +#include +#include + +// 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(EGL_SYNC_NATIVE_FENCE_ANDROID)); + ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_CONDITION, &value)); + EXPECT_EQ(value, static_cast(EGL_SYNC_NATIVE_FENCE_SIGNALED_ANDROID)); + ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, sync, EGL_SYNC_STATUS, &value)); + EXPECT_EQ(value, static_cast(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(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(EGL_SIGNALED)); + ASSERT_TRUE(fixture->State.GetSyncAttrib(fixture->Display, done, EGL_SYNC_CONDITION, &value)); + EXPECT_EQ(value, static_cast(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 diff --git a/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp b/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp index ef338c7f..1fd0ebbb 100644 --- a/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp +++ b/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp @@ -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(EGL::GetProcAddress("eglCreateSyncKHR")); + const auto destroySync = reinterpret_cast(EGL::GetProcAddress("eglDestroySyncKHR")); + const auto clientWait = reinterpret_cast(EGL::GetProcAddress("eglClientWaitSyncKHR")); + const auto getSyncAttrib = reinterpret_cast(EGL::GetProcAddress("eglGetSyncAttribKHR")); + const auto waitSync = reinterpret_cast(EGL::GetProcAddress("eglWaitSyncKHR")); + const auto dupFence = reinterpret_cast(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(EGL::GetProcAddress("glGenTextures")); + const auto bindTexture = reinterpret_cast(EGL::GetProcAddress("glBindTexture")); + const auto imageTarget = reinterpret_cast(EGL::GetProcAddress("glEGLImageTargetTexture2DOES")); + const auto genFramebuffers = reinterpret_cast(EGL::GetProcAddress("glGenFramebuffers")); + const auto bindFramebuffer = reinterpret_cast(EGL::GetProcAddress("glBindFramebuffer")); + const auto framebufferTexture = reinterpret_cast(EGL::GetProcAddress("glFramebufferTexture2D")); + const auto checkStatus = reinterpret_cast(EGL::GetProcAddress("glCheckFramebufferStatus")); + const auto clearColor = reinterpret_cast(EGL::GetProcAddress("glClearColor")); + const auto clear = reinterpret_cast(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(image.Offset), EGL_DMA_BUF_PLANE0_PITCH_EXT, + static_cast(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 + 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(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(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(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); + } +}