diff --git a/MobileGL/MG_Backend/BackendObject.cpp b/MobileGL/MG_Backend/BackendObject.cpp index e7250c18..a1949e99 100644 --- a/MobileGL/MG_Backend/BackendObject.cpp +++ b/MobileGL/MG_Backend/BackendObject.cpp @@ -388,6 +388,16 @@ namespace MobileGL::MG_Backend { m_windowHandle = {}; } + namespace { + MG_Util::Damage::Region& PresentDamageSlot() { + thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full(); + return damage; + } + } // namespace + + const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); } + void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; } + Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { const std::lock_guard lock(m_eglStateMutex); if (!m_eglDisplayInitialized || m_eglDisplay != dpy) { diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 79aa2b80..a2c92af9 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -11,6 +11,7 @@ #include "MG_State/GLState/TextureState/TextureEnum.h" #if MOBILEGL_BUILD_DISAGGREGATED #include +#include #endif namespace MobileGL { @@ -611,6 +612,12 @@ namespace MobileGL { Uint64 Modifier = 0; }; + // THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it + // around the backend's Present (from the present record), and a backend that can hand damage + // to its native present reads it there. Full whenever nothing set it. + const MG_Util::Damage::Region& CurrentPresentDamage(); + void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage); + class BackendObject { public: virtual ~BackendObject() = default; @@ -628,11 +635,24 @@ namespace MobileGL { // when the write generation moved, and at its frame boundary publishes its frame's fence // with PublishRead / ReadTracker. The application's GL state is untouched. False = not // supported / failed (logged). - virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) { - (void)image; + // + // `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy; + // Full copies all of it. The rest of the image is left as it is - the client sends a + // region only for an image that already holds the frame outside it - and a backend that + // cannot be sure the image's contents survived (a target it has never written) copies + // all of it regardless. + virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, + const MG_Util::Damage::Region& region) { + (void)image, (void)region; return false; } + // EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply) + // thread: how many presents ago the buffer the next frame draws into held the frame then + // presented; 0 = unknown (the application repaints everything). Asking says the client + // relies on the answer, so from then on the backend keeps what it presents. + virtual Int32 QueryCurrentBufferAge() { return 0; } + // ---- shared images, CLIENT side (BackendObject_Remote implements these) ---------- // // A new image, allocated by the server; `out->Fd` is the caller's to close. @@ -652,9 +672,16 @@ namespace MobileGL { return false; } // In the GL stream: the current context's default framebuffer, copied top row first into - // the image (BlitDefaultFramebufferToSharedImage on the server). Returns when it completed. - virtual Bool PresentToSharedImage(Uint64 id) { - (void)id; + // the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for + // all. Returns when it completed. + virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) { + (void)id, (void)region; + return false; + } + // In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge + // on the server). False: the backend has no answer (the caller reports 0). + virtual Bool QueryBufferAge(EGLint* age) { + (void)age; return false; } // In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as @@ -686,6 +713,12 @@ namespace MobileGL { virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height); virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw); + // eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native + // present. A backend with nothing to hand it to swaps as SwapEGLBuffers does. + virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) { + (void)damage; + return SwapEGLBuffers(dpy, draw); + } // Forwards the app-requested eglSwapInterval to the backend's native // presentation path (no-op for backends without a SetSwapInterval hook). virtual void SetEGLSwapInterval(Int interval); diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 2ae4549c..8a25e997 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1212,15 +1212,18 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image) { + Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image, + const MG_Util::Damage::Region& region) { #if MOBILEGL_BUILD_DISAGGREGATED - return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image); + return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region); #else - (void)image; + (void)image, (void)region; return false; #endif } + Int32 BackendObject_DirectGLES::QueryCurrentBufferAge() { return DirectGLES::CurrentDrawBufferAge(); } + 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 2f8d0e64..d1570c84 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -44,7 +44,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is // the image's EGLImage, ordered after the image's pending reads and published as a native // fence without a CPU wait (DirectGLES.h, SharedImageImpl). - Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override; + Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, + const MG_Util::Damage::Region& region) override; + // A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content, + // so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge). + Int32 QueryCurrentBufferAge() 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 f302f5ff..748bbada 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -16275,6 +16275,9 @@ namespace MobileGL::MG_Backend::DirectGLES { struct SurfaceEntry { NativeWindowType Window; EGLSurface Surface; + // Presents made on this native surface since it was created: a pbuffer's content is + // the last presented frame once there has been one (its buffer age is then 1). + Uint64 Presents = 0; }; Vector Surfaces; // The surface the session currently draws to / reads from - what a bind to another context @@ -17103,6 +17106,58 @@ namespace MobileGL::MG_Backend::DirectGLES { EGLSurface CurrentContextDrawSurface() { return ActiveNativeContext().Draw; } + namespace { + // Whether the driver of `display` has EGL_EXT_buffer_age (or EGL_KHR_partial_update, which + // defines the same query). Asked once per display. + // EGL_BUFFER_AGE_EXT (= EGL_BUFFER_AGE_KHR), spelled out: this file sees only the core EGL header. + constexpr EGLint kEglBufferAge = 0x313D; + + Bool NativeBufferAgeSupported() { + static EGLDisplay asked = EGL_NO_DISPLAY; + static Bool supported = false; + if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) return false; + if (asked != g_Display) { + asked = g_Display; + const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); + supported = extensions != nullptr && (std::strstr(extensions, "EGL_EXT_buffer_age") != nullptr || + std::strstr(extensions, "EGL_KHR_partial_update") != nullptr); + MGLOG_I("DirectGLES: the driver %s EGL_EXT_buffer_age", supported ? "has" : "lacks"); + } + return supported; + } + + // The calling session's record of `surface`; the registry lock is held by the caller (another + // session's window bring-up may erase stale entries of this one). + NativeSessionState::SurfaceEntry* FindSessionSurfaceLocked(NativeSessionState& session, EGLSurface surface) { + for (auto& entry : session.Surfaces) { + if (entry.Surface == surface) return &entry; + } + return nullptr; + } + } // namespace + + Int32 CurrentDrawBufferAge() { + NativeSessionState& session = ActiveNativeSession(); + const EGLSurface surface = session.Draw; + if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return 0; + Bool window = false; + Uint64 presents = 0; + { + const std::lock_guard lock(g_nativeRegistryMutex); + const auto* entry = FindSessionSurfaceLocked(session, surface); + if (entry == nullptr) return 0; + window = entry->Window != static_cast(0); + presents = entry->Presents; + } + if (!window) return presents > 0 ? 1 : 0; + // The driver's answer. On Android the query dequeues the buffer the next frame draws into - + // what the frame's first draw would do anyway, and the client asks before it draws. + if (presents == 0 || !NativeBufferAgeSupported() || !g_EGLFuncs.eglQuerySurface) return 0; + EGLint age = 0; + if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &age) || age < 0) return 0; + return age; + } + Bool SelectSessionSurface(EGLSurface surface) { NativeSessionState& session = ActiveNativeSession(); if (!SessionOwnsSurface(session, surface)) return false; @@ -18101,12 +18156,27 @@ namespace MobileGL::MG_Backend::DirectGLES { // defines it as "the entire surface is implicitly damaged and the behaviour is // equivalent to calling eglSwapBuffers" - and the wire protocol carries no damage // rectangles for the server to forward, so the full surface is what is meant anyway. + // + // The damage is the client's, from the present record (CurrentPresentDamage): GL window + // coordinates, which is what the native call takes. Whole surface = no rectangles. const EGLSurface drawSurface = ActiveNativeSession().Draw; if (g_EGLFuncs.eglSwapBuffersWithDamageEXT != nullptr) { - g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, nullptr, 0); + const MG_Util::Damage::Region& damage = MG_Backend::CurrentPresentDamage(); + EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; + EGLint count = 0; + if (!damage.IsFull() && !damage.IsEmpty()) { + count = static_cast(MG_Util::Damage::PackRects(damage, rects, MG_Util::Damage::kMaxRects)); + } + g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count); } else { g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); } + if (drawSurface != EGL_NO_SURFACE) { + // Resolved before the lock: ActiveNativeSession takes it too. + NativeSessionState& session = ActiveNativeSession(); + const std::lock_guard lock(g_nativeRegistryMutex); + if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents; + } if (canFence) PollFrameFences(); @@ -18458,6 +18528,8 @@ namespace MobileGL::MG_Backend::DirectGLES { EglImageRef Image; GLuint Renderbuffer = 0; Uint64 LastUse = 0; + // A present has written the image through this target (its content is a frame). + Bool Written = false; }; struct PresentTargets { std::mutex Mutex; @@ -18662,7 +18734,7 @@ namespace MobileGL::MG_Backend::DirectGLES { frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; } - Bool BlitDefaultFramebufferTo(const SharedImageView& view) { + Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { if (view.Id == 0 || view.Width == 0 || view.Height == 0 || view.NativeBuffer == nullptr) return false; if (!IsBackendContextCurrentOnThisThread()) { MGLOG_E_ONCE("Shared image present: no backend context is current on the calling thread"); @@ -18729,6 +18801,10 @@ namespace MobileGL::MG_Backend::DirectGLES { it = targets.ById.emplace(view.Id, std::move(target)).first; } it->second.LastUse = ++targets.UseClock; + // A target written before keeps what it was given; one new to this process is + // copied whole whatever the client asked (it cannot know the image's history here). + const Bool wholeTarget = !it->second.Written; + it->second.Written = true; GLuint& framebuffer = g_presentFramebuffers.Current(); if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer); @@ -18758,9 +18834,29 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(sourceHeight) == view.Height; // TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory // row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0. - g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, - static_cast(view.Height), static_cast(view.Width), 0, - GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR); + if (sameExtent && !wholeTarget && !region.IsFull()) { + // Only where the image differs from the frame, each rectangle flipped the + // same way (an empty region copies nothing: the image already is the frame). + MG_Util::Damage::Region clipped = region; + clipped.Normalize(sourceWidth, sourceHeight); + if (clipped.IsFull()) { + g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, + static_cast(view.Height), + static_cast(view.Width), 0, GL_COLOR_BUFFER_BIT, + GL_NEAREST); + } else { + for (const MG_Util::Damage::Rect& rect : clipped.Rects()) { + const MG_Util::Damage::Rect top = MG_Util::Damage::FlipY(rect, sourceHeight); + g_GLESFuncs.glBlitFramebuffer(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height, + top.X, top.Y + top.Height, top.X + top.Width, top.Y, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + } + } else { + g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, + static_cast(view.Height), static_cast(view.Width), + 0, GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR); + } const GLenum error = g_GLESFuncs.glGetError(); copied = error == GL_NO_ERROR; if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast(readBuffer)); @@ -18806,8 +18902,8 @@ namespace MobileGL::MG_Backend::DirectGLES { (void)image; return false; } - Bool BlitDefaultFramebufferTo(const SharedImageView& view) { - (void)view; + Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { + (void)view, (void)region; return false; } void AcquireForSampling(const EglImageRef& image) { (void)image; } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index c82e2d99..7d109ab7 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -116,6 +116,10 @@ namespace MobileGL::MG_Backend::DirectGLES { EGLSurface CurrentSessionDrawSurface(); // The surface the calling thread's ACTIVE context last drew to (EGL_NO_SURFACE for none). EGLSurface CurrentContextDrawSurface(); + // EGL_BUFFER_AGE_EXT of the session's current draw surface for the next frame: the driver's + // answer for a window surface (0 where the driver has no EGL_EXT_buffer_age), 1 for a pbuffer + // that has been presented (a pbuffer's content stays across swaps), 0 otherwise. + Int32 CurrentDrawBufferAge(); // Makes an EXISTING native surface of the calling thread's session the one it draws to, and // binds it to the active context. False for a surface the session does not own. Bool BindSessionSurface(EGLSurface surface); @@ -509,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // GL_TEXTURE_2D. False when the driver refused it (its GL error is consumed). Bool TargetBoundTexture2D(const EglImageRef& image); // BackendObject::BlitDefaultFramebufferToSharedImage, for the calling thread's context. - Bool BlitDefaultFramebufferTo(const SharedImageView& image); + Bool BlitDefaultFramebufferTo(const SharedImageView& image, const MG_Util::Damage::Region& region); // THE READER'S HALF of the image's sync state (SharedImageRegistry.h). Called on every use // of a name whose level 0 is `image`, before the command that samples it: when a write diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 7730b93d..d8205b24 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -568,7 +568,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { return BackendObject::SwapEGLBuffers(dpy, draw); } - Bool BackendObject_DirectVulkan::BlitDefaultFramebufferToSharedImage(const SharedImageView& image) { + Bool BackendObject_DirectVulkan::BlitDefaultFramebufferToSharedImage(const SharedImageView& image, + const MG_Util::Damage::Region& region) { #if MOBILEGL_BUILD_DISAGGREGATED // Same lock as SwapEGLBuffers: the copy reads the current surface target's image. const std::lock_guard lock(m_eglStateMutex); @@ -577,13 +578,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { static_cast(image.Id)); return false; } - return pVulkanRenderer->BlitDefaultFramebufferToSharedImage(image); + return pVulkanRenderer->BlitDefaultFramebufferToSharedImage(image, region); #else - (void)image; + (void)image, (void)region; return false; #endif } + Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge() { + const std::lock_guard lock(m_eglStateMutex); + return pVulkanRenderer ? pVulkanRenderer->CurrentDrawBufferAge() : 0; + } + void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) { // Same lock as SwapEGLBuffers: the renderer reads the request in Present. const std::lock_guard lock(m_eglStateMutex); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 7456514a..c2f469ef 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -38,7 +38,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override; Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override; void SetEGLSwapInterval(Int interval) override; - Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override; + Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, + const MG_Util::Damage::Region& region) override; + // The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge). + Int32 QueryCurrentBufferAge() 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/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index b30ab13c..ab45adbf 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -292,6 +292,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Fresh swapchain images hold garbage until a render pass stores into them. m_imageContentDefined.assign(imageCount, false); m_depthStencilContentDefined.assign(imageCount, false); + // Nothing presented from this chain yet: every buffer age is unknown. + m_ages.Reset(imageCount); CreateImageViews(device); CreateDepthStencilResources(device, physicalDevice); @@ -493,6 +495,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_imageLayouts.clear(); m_imageContentDefined.clear(); m_depthStencilContentDefined.clear(); + m_ages.Reset(0); m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h index 97ebf6a2..53d8896e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include "../VkIncludes.h" namespace MobileGL::MG_Backend::DirectVulkan { @@ -80,6 +81,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { void SetDepthStencilContentDefined(Uint32 index, Bool defined); void SetAllDepthStencilContentUndefined(); + // EGL_EXT_buffer_age over this chain's images. Ages start unknown with the chain (Create). + // Until a client asks for an age (NoteBufferAgeAsked) a presented image's content is + // treated as undefined, as before; once one has, a presented image keeps it - its age is + // counted and its content stays defined, so the next frame loads it. + void NoteBufferAgeAsked() { m_bufferAgeAsked = true; } + Bool BufferAgeAsked() const { return m_bufferAgeAsked; } + Int32 BufferAgeOf(Uint32 index) const { return m_ages.AgeOf(index); } + // Image `index` was presented. + void OnImagePresented(Uint32 index) { m_ages.OnPresented(index, m_bufferAgeAsked); } + // A swapped frame reached no image: every age is unknown. + void OnFrameDropped() { m_ages.OnFrameDropped(); } + private: void CreateImageViews(VkDevice device); void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice); @@ -109,5 +122,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_depthStencilImageLayouts; Vector m_imageContentDefined; Vector m_depthStencilContentDefined; + MG_Util::Damage::SwapchainAgeTracker m_ages; + // Kept across a rebuild: it is the application's habit, not the chain's. + Bool m_bufferAgeAsked = false; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index dcb809d1..267de68a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -14552,6 +14552,8 @@ void main() { // minimized-window app accumulates them for the whole // suspension. TryDrainFrameTransients(); + // The application counted this swap; no image did. + m_swapchainObject.OnFrameDropped(); MGLOG_D("Present skipped: no usable swapchain (zero-area window)"); return; } @@ -14699,7 +14701,13 @@ void main() { // behaviour), and EVERY ancillary depth/stencil buffer's content is // undefined after any swap. The render-pass manager turns the undefined // attachments' next tile loads into LOAD_OP_DONT_CARE. - m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, false); + // + // EGL_EXT_buffer_age changes the first half: once the client has asked for an age it + // repaints only what changed since the image it is given was last shown, so a presented + // image keeps its content (the wire passes load it; Vulkan preserves an image's content + // through a present and a layout transition out of PRESENT_SRC) and its age is counted. + m_swapchainObject.OnImagePresented(m_imageIndexAcquired); + if (!m_swapchainObject.BufferAgeAsked()) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, false); m_swapchainObject.SetAllDepthStencilContentUndefined(); // The authoritative check, done here - after the frame is presented, before the next // acquire. This is what makes a launcher-side resolution change take effect: shrinking @@ -14798,6 +14806,17 @@ void main() { m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } + Int32 VulkanRenderer::CurrentDrawBufferAge() { + if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) + return 0; + m_swapchainObject.NoteBufferAgeAsked(); + if (m_imageIndexAcquired >= m_swapchainObject.GetImageCount()) return 0; + // The image the next frame's first draw makes the default framebuffer's (it was acquired at + // the end of the last present). Its content is a frame only if nothing undefined it since. + if (!m_swapchainObject.IsImageContentDefined(m_imageIndexAcquired)) return 0; + return m_swapchainObject.BufferAgeOf(m_imageIndexAcquired); + } + void VulkanRenderer::CreateInstance() { #if defined(VK_USE_PLATFORM_METAL_EXT) // MoltenVK snapshots its configuration when the loader first discovers the ICD. Set diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 8502f166..bc92bc7a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -186,7 +186,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { // (SharedImages::PublishWrite); waits for it on the CPU only on a device without sync_file // export. False (logged) when it cannot be done here: no AHardwareBuffer import, a // quarter-turned surface, an unblittable format. - Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image); + // + // `region` (GL window coordinates; Full = all) is the part to copy into an image this + // renderer has written before - the rest of it is left as it is; a first write, a scaled + // or turned copy copies all of it. + Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, const MG_Util::Damage::Region& region); + // 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. + Int32 CurrentDrawBufferAge(); // The reading side's per-use hook (WireSharedImage.inc): every shared-image texture the // wire draw or dispatch being set up samples (VkTextureManager::NoteSharedImageUse) is // acquired from the foreign family before it, when it moved or is not held. @@ -699,6 +707,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkTextureManager::ImportedSharedImage image; WeakPtr owner; Uint64 lastSubmit = 0; + // A copy has written the image through this import: what it holds is a frame. + Bool written = false; }; UnorderedMap m_sharedImagePresentTargets; void DestroySharedImagePresentTargets(Bool onlyDead); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc index 2eb342b5..55d23e58 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc @@ -70,7 +70,8 @@ return destination == VK_FORMAT_R8G8B8A8_UNORM && source == VK_FORMAT_R8G8B8A8_SRGB; } - Bool VulkanRenderer::BlitDefaultFramebufferToSharedImage(const SharedImageView& view) { + Bool VulkanRenderer::BlitDefaultFramebufferToSharedImage(const SharedImageView& view, + const MG_Util::Damage::Region& region) { if (m_device == VK_NULL_HANDLE || view.Width == 0 || view.Height == 0) return false; // The debug knob's device check (MGPipeDebugDeviceLossDue) rides the frame boundary here. if (MG_Pipe::MGPipeDebugDeviceLossDue()) NoteDeviceLoss(VK_ERROR_DEVICE_LOST, "the debug knob (shared-image present)"); @@ -145,6 +146,18 @@ const Bool sameExtent = source.extent.width == view.Width && source.extent.height == view.Height; const Bool halfTurn = preTransform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR; const Bool copy = sameExtent && !halfTurn && SharedImageCopiesAsIs(source.format, destination.format); + // ONLY WHERE THE IMAGE DIFFERS FROM THE FRAME: an image this renderer wrote before, copied + // one to one (no scaling, no half turn). Its other texels must survive, so the acquire then + // keeps the layout the last reader released it in rather than discarding it. + MG_Util::Damage::Region part = region; + if (!part.IsFull()) part.Normalize(static_cast(view.Width), static_cast(view.Height)); + const Bool partial = !part.IsFull() && found->second.written && sameExtent && !halfTurn; + // The region's rectangles, top row first and made disjoint: a copy's regions may not overlap. + Vector rows; + if (partial) { + for (const MG_Util::Damage::Rect& rect : MG_Util::Damage::Disjoint(part.Rects())) + rows.push_back(MG_Util::Damage::FlipY(rect, static_cast(view.Height))); + } VkFormatProperties sourceProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties); if (!copy) { @@ -187,7 +200,8 @@ VkImageMemoryBarrier acquire{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; acquire.srcAccessMask = 0; acquire.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - acquire.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + // Every present releases the image in GENERAL, and so does every reader. + acquire.oldLayout = partial ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; acquire.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; acquire.dstQueueFamilyIndex = destination.queueFamily; @@ -197,7 +211,41 @@ 0, nullptr, 0, nullptr, 1, &acquire); const VkImageSubresourceLayers layers{VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - if (copy) { + if (partial) { + // An empty region copies nothing: the image already is the frame. + if (copy && !rows.empty()) { + Vector regions; + regions.reserve(rows.size()); + for (const MG_Util::Damage::Rect& row : rows) { + VkImageCopy one{}; + one.srcSubresource = layers; + one.dstSubresource = layers; + one.srcOffset = {row.X, row.Y, 0}; + one.dstOffset = {row.X, row.Y, 0}; + one.extent = {static_cast(row.Width), static_cast(row.Height), 1}; + regions.push_back(one); + } + vkCmdCopyImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(regions.size()), regions.data()); + } else if (!rows.empty()) { + Vector regions; + regions.reserve(rows.size()); + for (const MG_Util::Damage::Rect& row : rows) { + VkImageBlit one{}; + one.srcSubresource = layers; + one.dstSubresource = layers; + one.srcOffsets[0] = {row.X, row.Y, 0}; + one.srcOffsets[1] = {row.X + row.Width, row.Y + row.Height, 1}; + one.dstOffsets[0] = one.srcOffsets[0]; + one.dstOffsets[1] = one.srcOffsets[1]; + regions.push_back(one); + } + vkCmdBlitImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); + } + } else if (copy) { VkImageCopy region{}; region.srcSubresource = layers; region.dstSubresource = layers; @@ -253,6 +301,7 @@ return false; } found->second.lastSubmit = m_submitCounter; + found->second.written = true; const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE); int fence = -1; if (signal != VK_NULL_HANDLE && ExportSharedImageSignal(signal, fence)) { @@ -275,6 +324,7 @@ return false; } found->second.lastSubmit = m_submitCounter; + found->second.written = true; SI::PublishWrite(*image, -1); return true; } diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 86bf3cb8..ea936acb 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -391,7 +391,14 @@ namespace MobileGL::MG_Impl::EGLImpl { return surface; } - EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw) { + EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw) { return SwapBuffersWithDamage(dpy, draw, nullptr, 0); } + + // EGL_KHR/EXT_swap_buffers_with_damage. The rectangles (GL window coordinates, origin + // bottom-left; none = the whole surface) are clipped to the surface and fitted into a record. + // A Wayland window copies only what its shared image missed and damages only the frame's + // rectangles; a server-owned window hands them to the native present. Either way they are a + // hint about what changed, never a limit on what is shown. + EGLBoolean SwapBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, EGLint n_rects) { const std::lock_guard operationLock(EGLOperationMutex()); #if MOBILEGL_BUILD_DISAGGREGATED // The present is this thread's context's (GLStreamScope in EGLImpl.h). @@ -411,15 +418,31 @@ namespace MobileGL::MG_Impl::EGLImpl { MGLOG_E_ONCE("activeBackendObject not initialized!"); return EGL_FALSE; } + if (n_rects < 0 || (n_rects > 0 && rects == nullptr)) { + state->SetError(EGL_BAD_PARAMETER); + return EGL_FALSE; + } + MG_Util::Damage::Region damage = MG_Util::Damage::Region::FromEglRects(rects, n_rects); + if (!damage.IsFull()) { + EGLint width = 0; + EGLint height = 0; + if (state->QuerySurface(dpy, draw, EGL_WIDTH, &width) && state->QuerySurface(dpy, draw, EGL_HEIGHT, &height)) { + damage.Normalize(width, height); + } else { + damage.SetFull(); + } + } #if MOBILEGL_WAYLAND_WINDOWS // A Wayland window shows what is attached to it: the frame is read back and attached // BEFORE the swap, while it is still the drawable's content. - if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end() && !it->second->Present()) { + if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end() && !it->second->Present(damage)) { state->SetError(EGL_BAD_SURFACE); return EGL_FALSE; } #endif - if (!backendObject->SwapEGLBuffers(dpy, draw)) { + const Bool swapped = damage.IsFull() ? backendObject->SwapEGLBuffers(dpy, draw) + : backendObject->SwapEGLBuffersWithDamage(dpy, draw, damage); + if (!swapped) { MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw); state->SetError(EGL_BAD_SURFACE); return EGL_FALSE; @@ -728,6 +751,37 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_FALSE; } + // EGL_EXT_buffer_age. The buffers are the server's, so the server answers - in the stream, + // behind the records already written, for the buffer the next frame draws into. Only for the + // calling thread's draw surface: that is the buffer the question is about (and the stream is + // bound to its context). + if (attribute == EGL_BUFFER_AGE_EXT) { + const std::lock_guard operationLock(EGLOperationMutex()); + if (!SharedImagesAvailable()) { + state->SetError(EGL_BAD_ATTRIBUTE); + return EGL_FALSE; + } + if (!state->ValidateSurfaceOnDisplay(display, surface)) { + state->SetError(EGL_BAD_SURFACE); + return EGL_FALSE; + } + if (value == nullptr) { + state->SetError(EGL_BAD_PARAMETER); + return EGL_FALSE; + } + if (state->GetCurrentSurface(EGL_DRAW) != surface) { + state->SetError(EGL_BAD_SURFACE); + return EGL_FALSE; + } +#if MOBILEGL_BUILD_DISAGGREGATED + if (StreamGateActive()) StreamBindCallingThreadLocked(); +#endif + EGLint age = 0; + auto* backendObject = MG_Backend::pActiveBackendObject.get(); + if (backendObject == nullptr || !backendObject->QueryBufferAge(&age)) age = 0; + *value = age; + return EGL_TRUE; + } // A size that changes under the application (a server-owned window resized) arrives from the // backend: let it bring the surface's record up to date first, so a caller that polls the size // while it draws nothing still sees the change. diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h index 0d7ac22a..58412786 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h @@ -16,6 +16,7 @@ namespace MobileGL::MG_Impl::EGLImpl { EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window, const EGLint* attrib_list); EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw); + EGLBoolean SwapBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, EGLint n_rects); EGLBoolean ChooseConfig(EGLDisplay dpy, const EGLint* attrib_list, EGLConfig* configs, EGLint config_size, EGLint* num_config); EGLContext CreateContext(EGLDisplay dpy, EGLConfig config, EGLContext shareCtx, const EGLint* attrib_list); diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index 7f33dfd2..236cbadc 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -23,11 +23,13 @@ namespace MobileGL::MG_Impl::EGLImpl { "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS; // The display list again, with the dma-buf import a backend with shared images has (a split // client: the server allocates the images and names the descriptors that come back). A - // monolith has no allocator to recognise a dma-buf with, so it never advertises these. + // monolith has no allocator to recognise a dma-buf with, so it never advertises these. The + // same server answers the buffer age of its surfaces and takes swap damage to its presents. inline constexpr const char* kDisplayExtensionStringWithSharedImages = "EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base " "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_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers" + " EGL_EXT_buffer_age EGL_KHR_swap_buffers_with_damage EGL_EXT_swap_buffers_with_damage"; 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 34cdc33e..bd8f1b8b 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp @@ -154,6 +154,27 @@ MOBILEGL_EGL_API EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface draw) { return MobileGL::MG_Impl::EGLImpl::SwapBuffers(dpy, draw); } +// EGL_KHR_swap_buffers_with_damage and EGL_EXT_swap_buffers_with_damage: one function, two names. +MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects) { + const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock; + MGLOG_D("eglSwapBuffersWithDamageKHR(dpy=%p, draw=%p, n_rects=%d)", dpy, draw, n_rects); +#ifdef TRACY_ENABLE + FrameMark; +#endif + return MobileGL::MG_Impl::EGLImpl::SwapBuffersWithDamage(dpy, draw, rects, n_rects); +} + +MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects) { + const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock; + MGLOG_D("eglSwapBuffersWithDamageEXT(dpy=%p, draw=%p, n_rects=%d)", dpy, draw, n_rects); +#ifdef TRACY_ENABLE + FrameMark; +#endif + return MobileGL::MG_Impl::EGLImpl::SwapBuffersWithDamage(dpy, draw, rects, n_rects); +} + MOBILEGL_EGL_API EGLSurface eglCreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint* attrib_list) { const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock; MGLOG_D("eglCreatePbufferSurface(dpy=%p, config=%p, attrib_list=%p)", dpy, config, attrib_list); diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp index 002587e9..fa0ef9df 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp @@ -36,6 +36,10 @@ extern "C" EGLImage eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint* attrib_list); extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImage image); +extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects); +extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects); 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, @@ -234,6 +238,8 @@ namespace { {"eglCreateImageKHR", reinterpret_cast(eglCreateImageKHR)}, {"eglDestroyImageKHR", reinterpret_cast(eglDestroyImageKHR)}, {"eglQueryDmaBufFormatsEXT", reinterpret_cast(eglQueryDmaBufFormatsEXT)}, + {"eglSwapBuffersWithDamageKHR", reinterpret_cast(eglSwapBuffersWithDamageKHR)}, + {"eglSwapBuffersWithDamageEXT", reinterpret_cast(eglSwapBuffersWithDamageEXT)}, {"eglQueryDmaBufModifiersEXT", reinterpret_cast(eglQueryDmaBufModifiersEXT)}, {"eglCreatePlatformWindowSurface", reinterpret_cast(eglCreatePlatformWindowSurface)}, {"eglCreatePlatformPixmapSurface", reinterpret_cast(eglCreatePlatformPixmapSurface)}, diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp index d0d987de..ff16472d 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp @@ -320,6 +320,13 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { Bool useDmabuf = false; uint32_t fourcc = kDrmFormatXbgr8888; DmabufBuffer dmabufBuffers[3]; + // Per shared image, what the frames written into the other images changed since this one + // was last written: what its next write has to bring over besides the frame's own damage. + MG_Util::Damage::BufferDamageTracker dmabufDamage{3}; + // The size of the last buffer committed: a commit at another size (or the first) damages + // the whole buffer, whatever the frame says. + EGLint committedWidth = 0; + EGLint committedHeight = 0; ~Impl() { const Api& api = WaylandApi(); @@ -479,6 +486,8 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { return false; } close(image.Fd); + // A new image holds nothing of any frame yet. + dmabufDamage.Invalidate(static_cast(&buffer - dmabufBuffers)); buffer.buffer = wlBuffer; buffer.imageId = image.Id; buffer.width = width; @@ -510,20 +519,33 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { // The rest of this window's life is wl_shm's. void FallBackToShm() { for (auto& buffer : dmabufBuffers) Release(buffer); + dmabufDamage.InvalidateAll(); useDmabuf = false; } - // `wlBuffer` becomes the window's content: attach, damage, commit. - void Commit(wl_proxy* wlBuffer) { + // `wlBuffer` becomes the window's content: attach, damage, commit. `damage` is the frame's + // (GL window coordinates); a buffer's rows run top-down, so each rectangle is flipped. A + // compositor without damage_buffer (wl_surface < 4), a new size or the first commit is + // damaged whole. + void Commit(wl_proxy* wlBuffer, const MG_Util::Damage::Region& damage) { const Api& api = WaylandApi(); wl_proxy* surface = window->surface; const uint32_t surfaceVersion = api.getVersion(surface); api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, wlBuffer, window->dx, window->dy); - if (surfaceVersion >= 4) { + const Bool whole = damage.IsFull() || surfaceVersion < 4 || committedWidth != width || + committedHeight != height || window->dx != 0 || window->dy != 0; + if (!whole) { + for (const MG_Util::Damage::Rect& rect : damage.FlippedY(height).Rects()) { + api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, rect.X, rect.Y, + rect.Width, rect.Height); + } + } else if (surfaceVersion >= 4) { api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, width, height); } else { api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, width, height); } + committedWidth = width; + committedHeight = height; api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0); window->attached_width = width; window->attached_height = height; @@ -670,20 +692,25 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { return true; } - Bool WindowSurface::Present() { + Bool WindowSurface::Present(const MG_Util::Damage::Region& damage) { Impl& impl = *m_impl; if (impl.window == nullptr) return false; if (impl.useDmabuf) { // The server copies the frame into the image GPU-side, top row first - a wl_buffer's // first row - and answers once the copy has completed, so the compositor reads a - // finished frame. + // finished frame. Only where the image differs from the frame: the rest of it already + // is the frame. DmabufBuffer* buffer = impl.NextFreeDmabuf(); auto* backendObject = MG_Backend::pActiveBackendObject.get(); - if (buffer != nullptr && backendObject != nullptr && backendObject->PresentToSharedImage(buffer->imageId)) { - impl.Commit(buffer->buffer); - buffer->busy = true; - return true; + if (buffer != nullptr && backendObject != nullptr) { + const SizeT index = static_cast(buffer - impl.dmabufBuffers); + const MG_Util::Damage::Region copy = impl.dmabufDamage.TakeForWrite(index, damage, impl.width, impl.height); + if (backendObject->PresentToSharedImage(buffer->imageId, copy)) { + impl.Commit(buffer->buffer, damage); + buffer->busy = true; + return true; + } } MGLOG_E_ONCE("Wayland: a frame could not be presented through a linux-dmabuf shared image; the window " "falls back to wl_shm"); @@ -699,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { // GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A. ReadBackFrameBGRA(impl.width, impl.height, impl.scratch, static_cast(buffer->map), static_cast(buffer->stride)); - impl.Commit(buffer->buffer); + impl.Commit(buffer->buffer, MG_Util::Damage::Region::Full()); buffer->busy = true; return true; } diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h index 9fc1f8dc..01b6041c 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h @@ -34,6 +34,7 @@ #if MOBILEGL_WAYLAND_WINDOWS #include +#include namespace MobileGL::MG_Impl::EGLImpl { // The current context's default-framebuffer frame, `width` x `height`, written top-down in @@ -76,7 +77,12 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland { // pack state put back exactly as they were), then attach, damage, commit. A shared-image // present that fails moves the window to wl_shm for good. False (logged) when the frame // did not reach the window; the swap itself goes on regardless. - Bool Present(); + // + // `damage` (GL window coordinates, clipped to the window; Full = all of it) is what the + // frame changed. A shared image is copied only where it differs from the frame - the + // frame's damage and every frame's since that image was last written - and the surface is + // damaged only where the frame changed, flipped to the buffer's top-left origin. + Bool Present(const MG_Util::Damage::Region& damage); // wl_egl_window_resize's effect, taken: true (with the new size) when the application gave // its wl_egl_window a size this presentation is not at yet. The presentation adopts it - diff --git a/MobileGL/MG_Impl/GetProcAddress.cpp b/MobileGL/MG_Impl/GetProcAddress.cpp index c9a46478..29ecb924 100644 --- a/MobileGL/MG_Impl/GetProcAddress.cpp +++ b/MobileGL/MG_Impl/GetProcAddress.cpp @@ -18,6 +18,10 @@ extern "C" EGLDisplay eglGetPlatformDisplayEXT(EGLenum platform, void* native_di extern "C" EGLImage eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint* attrib_list); extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImage image); +extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects); +extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface draw, const EGLint* rects, + EGLint n_rects); 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, @@ -76,6 +80,8 @@ namespace MobileGL::MG_Impl { GETPROC(eglCreateImageKHR, name); GETPROC(eglDestroyImageKHR, name); GETPROC(eglQueryDmaBufFormatsEXT, name); + GETPROC(eglSwapBuffersWithDamageKHR, name); + GETPROC(eglSwapBuffersWithDamageEXT, name); GETPROC(eglQueryDmaBufModifiersEXT, name); GETPROC(eglGetPlatformDisplay, name); GETPROC(eglGetPlatformDisplayEXT, name); diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 7de5801f..c1b7b6c1 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -1804,10 +1804,21 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPFlush, 8); + // DAMAGE ON THE WIRE (eglSwapBuffersWithDamage, docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md + // item 5). At most kMGPMaxDamageRects rectangles, each x, y, width, height in GL window + // coordinates - origin at the bottom-left, as EGL hands them over. A count of 0 means the whole + // surface, which is also what every record from a client that sends no damage says. + inline constexpr Uint32 kMGPMaxDamageRects = 16; + struct MGPPresent { Uint64 FrameSerial; + // The frame's damage: what the server hands to the native present (swap-with-damage, an + // incremental present). A hint for what is shown; it never decides what is drawn. + Uint32 DamageCount; + Uint32 Pad0; + Int32 Damage[kMGPMaxDamageRects * 4]; }; - MGP_ASSERT_POD(MGPPresent, 8); + MGP_ASSERT_POD(MGPPresent, 272); struct MGPSwapInterval { Int32 Interval; @@ -1994,6 +2005,15 @@ namespace MobileGL::MG_Pipe { // when the copy has completed on the GPU. // Attach the texture Texture's storage becomes ImageId (glEGLImageTargetTexture2DOES); // it lasts until the texture's next storage-defining respecify. + // QueryBufferAge EGL_BUFFER_AGE_EXT of the current context's draw surface, answered in the + // reply's BufferAge: how many presents ago the buffer the next frame draws into held + // the frame then presented, 0 = unknown. Asked in the stream, so it is answered + // for the buffer that frame's records will draw into. + // + // 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 + // image missed since it was last written, and sends that, so the rest of the image is already + // the frame. // // ImageId is a server-minted id, never reused within the server's life. enum MGPSharedImageOpKind : Uint32 { @@ -2002,6 +2022,7 @@ namespace MobileGL::MG_Pipe { kMGPSharedImageRelease = 3, kMGPSharedImagePresent = 4, kMGPSharedImageAttach = 5, + kMGPSharedImageQueryBufferAge = 6, }; struct MGPSharedImageOp { @@ -2011,8 +2032,12 @@ namespace MobileGL::MG_Pipe { Uint32 Height; Uint64 ImageId; MGPipeHandle Texture; // Attach only + // Present only: the region to copy (see above). + Uint32 DamageCount; + Uint32 Pad0; + Int32 Damage[kMGPMaxDamageRects * 4]; }; - MGP_ASSERT_POD(MGPSharedImageOp, 32); + MGP_ASSERT_POD(MGPSharedImageOp, 296); struct MGPSharedImageReply { Uint64 ImageId; @@ -2022,7 +2047,7 @@ namespace MobileGL::MG_Pipe { Uint32 Format; Uint32 Stride; Uint32 Offset; - Uint32 Pad0; + Int32 BufferAge; // QueryBufferAge only }; MGP_ASSERT_POD(MGPSharedImageReply, 40); diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index b4a513b4..731826c7 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -264,7 +264,7 @@ F(Flags) #define MGP_FIELDS_MGPPresent(F) \ - F(FrameSerial) + F(FrameSerial) F(DamageCount) F(Damage) #define MGP_FIELDS_MGPSwapInterval(F) \ F(Interval) @@ -303,7 +303,7 @@ // shared_image's POD. #define MGP_FIELDS_MGPSharedImageOp(F) \ - F(Op) F(Format) F(Width) F(Height) F(ImageId) F(Texture) + F(Op) F(Format) F(Width) F(Height) F(ImageId) F(Texture) F(DamageCount) F(Damage) // P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry // array is one field, exactly as MGPFramebufferState's Color[8] is one. diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index 459abcf7..1e80dfff 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -388,6 +388,8 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = { {"MGPFlush.Flags", offsetof(MGPFlush, Flags), sizeof(((MGPFlush*)nullptr)->Flags)}, {"MGPPresent", 0, sizeof(MGPPresent)}, {"MGPPresent.FrameSerial", offsetof(MGPPresent, FrameSerial), sizeof(((MGPPresent*)nullptr)->FrameSerial)}, + {"MGPPresent.DamageCount", offsetof(MGPPresent, DamageCount), sizeof(((MGPPresent*)nullptr)->DamageCount)}, + {"MGPPresent.Damage", offsetof(MGPPresent, Damage), sizeof(((MGPPresent*)nullptr)->Damage)}, {"MGPSwapInterval", 0, sizeof(MGPSwapInterval)}, {"MGPSwapInterval.Interval", offsetof(MGPSwapInterval, Interval), sizeof(((MGPSwapInterval*)nullptr)->Interval)}, {"MGPSurfaceInfo", 0, sizeof(MGPSurfaceInfo)}, @@ -440,6 +442,8 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = { {"MGPSharedImageOp.Height", offsetof(MGPSharedImageOp, Height), sizeof(((MGPSharedImageOp*)nullptr)->Height)}, {"MGPSharedImageOp.ImageId", offsetof(MGPSharedImageOp, ImageId), sizeof(((MGPSharedImageOp*)nullptr)->ImageId)}, {"MGPSharedImageOp.Texture", offsetof(MGPSharedImageOp, Texture), sizeof(((MGPSharedImageOp*)nullptr)->Texture)}, + {"MGPSharedImageOp.DamageCount", offsetof(MGPSharedImageOp, DamageCount), sizeof(((MGPSharedImageOp*)nullptr)->DamageCount)}, + {"MGPSharedImageOp.Damage", offsetof(MGPSharedImageOp, Damage), sizeof(((MGPSharedImageOp*)nullptr)->Damage)}, {"MGPContextValues", 0, sizeof(MGPContextValues)}, {"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)}, {"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)}, diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index f19dc584..4edd2ccb 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -422,6 +422,18 @@ namespace MobileGL::MG_Remote::Client { return MG_Backend::BackendObject::SwapEGLBuffers(dpy, draw); } + // The damage rides in the present record the base's Present emitter writes (EmitPresent takes it + // from the session); the swap is otherwise the plain one above. + Bool BackendObject_Remote::SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, + const MG_Util::Damage::Region& damage) { + ClientSession* session = ClientSession::Active(); + if (session != nullptr) session->SetPendingPresentDamage(damage); + const Bool swapped = SwapEGLBuffers(dpy, draw); + // A swap refused before it reached the emitter must not leave its damage for the next one. + if (session != nullptr) session->SetPendingPresentDamage(MG_Util::Damage::Region::Full()); + return swapped; + } + // No SetEGLSwapInterval here any more (P10 B): the base class calls the emit table's slot, // which is now Server::ServerSetEGLSwapInterval itself (EmitTables.cpp). @@ -491,15 +503,28 @@ namespace MobileGL::MG_Remote::Client { return session->EmitSharedImage(op, -1, nullptr, nullptr); } - Bool BackendObject_Remote::PresentToSharedImage(Uint64 id) { + Bool BackendObject_Remote::PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) { ClientSession* session = ClientSession::Active(); if (session == nullptr) return false; MG_Pipe::MGPSharedImageOp op{}; op.Op = MG_Pipe::kMGPSharedImagePresent; op.ImageId = id; + op.DamageCount = MG_Util::Damage::PackRects(region, op.Damage, MG_Pipe::kMGPMaxDamageRects); return session->EmitSharedImage(op, -1, nullptr, nullptr); } + // In the stream, so the server answers for the buffer the records after it draw into. + Bool BackendObject_Remote::QueryBufferAge(EGLint* age) { + ClientSession* session = ClientSession::Active(); + if (session == nullptr || age == nullptr) return false; + MG_Pipe::MGPSharedImageOp op{}; + op.Op = MG_Pipe::kMGPSharedImageQueryBufferAge; + MG_Pipe::MGPSharedImageReply reply{}; + if (!session->EmitSharedImage(op, -1, &reply, nullptr)) return false; + *age = std::max(reply.BufferAge, 0); + return true; + } + Bool BackendObject_Remote::AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) { ClientSession* session = ClientSession::Active(); if (session == nullptr) return false; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index 6580ceff..beb0c154 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -78,6 +78,7 @@ namespace MobileGL::MG_Remote::Client { Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override; Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override; Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override; + Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) override; void ReleaseEGLSurface(EGLSurface surface) override; void ReleaseEGLResources() override; @@ -86,7 +87,8 @@ namespace MobileGL::MG_Remote::Client { MG_Backend::SharedImageExport* out) override; Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) override; Bool ReleaseSharedImage(Uint64 id) override; - Bool PresentToSharedImage(Uint64 id) override; + Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) override; + Bool QueryBufferAge(EGLint* age) override; Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) override; // Copies the caps mirror's FormatCapabilityCache into the base class's diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 8cf206e7..8be79199 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -41,6 +41,7 @@ #include #include +#include #include "../Server/ServerSpawn.h" #include "../Server/SurfaceControlFrame.h" @@ -354,6 +355,19 @@ namespace MobileGL::MG_Remote::Client { // reservation across the park. Uint64 AcquirePresentCredit(); + // eglSwapBuffersWithDamage's damage for the present about to be emitted (BackendObject_Remote + // sets it around the swap; EmitPresent takes it into the record). Taking clears it, so a + // plain swap after it carries none (the whole surface). + void SetPendingPresentDamage(const MG_Util::Damage::Region& damage) { + m_pendingDamageCount = MG_Util::Damage::PackRects(damage, m_pendingDamage, MG_Pipe::kMGPMaxDamageRects); + } + Uint32 TakePendingPresentDamage(Int32* out) { + const Uint32 count = m_pendingDamageCount; + std::memcpy(out, m_pendingDamage, sizeof(Int32) * 4 * count); + m_pendingDamageCount = 0; + return count; + } + // R-1's invariant made checkable rather than only written down: true while this // thread is inside a barrier wait. The apply thread sets its own flag on entry to the // applier; a debug/verify build asserts the two are never both true, and that the @@ -560,6 +574,8 @@ namespace MobileGL::MG_Remote::Client { // §2.4: presents published (the 1-based FrameSerial space) and how often the credit // wait really blocked. Uint64 m_presentsSent = 0; + Uint32 m_pendingDamageCount = 0; + Int32 m_pendingDamage[MG_Pipe::kMGPMaxDamageRects * 4] = {}; Uint64 m_presentCreditWaits = 0; // sm: the spawn client owns ONE end and two bells. Held here rather diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index f392e78b..abfcbf80 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -1186,6 +1186,7 @@ namespace MobileGL::MG_Remote::Client { // With run-ahead disarmed this is a counter and nothing else, and the record below // travels exactly as it did - the present row's own barrier is the pacing there. record.FrameSerial = session.AcquirePresentCredit(); + record.DamageCount = session.TakePendingPresentDamage(record.Damage); session.EmitAndWait(MG_Pipe::MGPWireOp::Present, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 20d6fac3..5d3af20a 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -389,6 +389,15 @@ namespace MobileGL::MG_Remote::Server { return true; } + namespace { + // A record's damage rectangles as a region; 0 rectangles is the whole surface. A count past + // what the record holds is clamped (the rectangles it names are all there are). + MG_Util::Damage::Region DamageOfRecord(Uint32 count, const Int32* rects) { + const Int32 n = static_cast(std::min(count, MG_Pipe::kMGPMaxDamageRects)); + return MG_Util::Damage::Region::FromEglRects(rects, n); + } + } // namespace + Bool ServerVerbSink::OnPresent(const MG_Pipe::MGPPresent& present) { const MG_Backend::GlobalBackendFunctionsTable* table = Table("present"); if (table == nullptr) return false; @@ -407,7 +416,10 @@ namespace MobileGL::MG_Remote::Server { // TrimBufferPool's recycle watermark stop tracking frames - which is the reason the 1:1 // was wanted, recorded here so a future swap-without-present is looked for rather than // assumed impossible. Presents() is exposed for a lane that wants to make the comparison. + // The frame's damage rides to the native present for the length of this call. + MG_Backend::SetCurrentPresentDamage(DamageOfRecord(present.DamageCount, present.Damage)); table->Present(); + MG_Backend::SetCurrentPresentDamage(MG_Util::Damage::Region::Full()); ++m_presents; // FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side // present credit, so the client sends 0 and the frame count on this side IS the serial. @@ -1558,10 +1570,17 @@ namespace MobileGL::MG_Remote::Server { view.Height = image->Height; view.Fourcc = image->Fourcc; view.NativeBuffer = image->Native; - if (!backend->BlitDefaultFramebufferToSharedImage(view)) return false; + if (!backend->BlitDefaultFramebufferToSharedImage(view, DamageOfRecord(op.DamageCount, op.Damage))) + return false; fill(*image); return true; } + case MG_Pipe::kMGPSharedImageQueryBufferAge: { + MG_Backend::BackendObject* backend = ServerLoopInstance().Backend(); + if (backend == nullptr) return false; + reply.BufferAge = backend->QueryCurrentBufferAge(); + return true; + } case MG_Pipe::kMGPSharedImageAttach: { SI::ImageRef image = holder.Get(op.ImageId); if (image == nullptr) return false; diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 3746d310..c6eb11f9 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -700,8 +700,10 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { EXPECT_EQ(sizeof(MGPBindContext), 8u); EXPECT_EQ(sizeof(MGPBindContext::ClientContextToken), 8u); // Shared images: the request, its answer and the aux-socket sideband are fixed-width PODs; the - // sideband is the T0 Offer's size so one inbox reads both. - EXPECT_EQ(sizeof(MGPSharedImageOp), 32u); + // sideband is the T0 Offer's size so one inbox reads both. The request carries a present's copy + // region (kMGPMaxDamageRects rectangles), as the present record carries its frame's damage. + EXPECT_EQ(sizeof(MGPSharedImageOp), 296u); + EXPECT_EQ(sizeof(MGPPresent), 272u); EXPECT_EQ(sizeof(MGPSharedImageReply), 40u); EXPECT_EQ(sizeof(MGPSharedImageFdOffer), 32u); EXPECT_EQ(sizeof(MGPHandleOnly), 16u); diff --git a/MobileGL/MG_Test/Util/DamageTest.cpp b/MobileGL/MG_Test/Util/DamageTest.cpp index 04c7daf1..2c07ee4c 100644 --- a/MobileGL/MG_Test/Util/DamageTest.cpp +++ b/MobileGL/MG_Test/Util/DamageTest.cpp @@ -100,6 +100,43 @@ TEST(Damage, NormalizeMergesDownToTheLimitAndStillCoversEverything) { EXPECT_EQ(three, Of({{0, 0, 20, 10}, {500, 500, 10, 10}})); } +// A copy whose regions may not overlap gets the same pixels, each once. +TEST(Damage, DisjointCoversTheSamePixelsOnce) { + const Vector input{{0, 0, 10, 10}, {5, 5, 10, 10}, {2, 2, 3, 3}, {20, 0, 4, 4}, {0, 8, 30, 2}}; + const Vector out = Disjoint(input); + for (Int32 y = 0; y < 40; ++y) { + for (Int32 x = 0; x < 40; ++x) { + Int32 inInput = 0, inOut = 0; + for (const Rect& r : input) inInput |= (x >= r.X && x < r.X + r.Width && y >= r.Y && y < r.Y + r.Height); + for (const Rect& r : out) inOut += (x >= r.X && x < r.X + r.Width && y >= r.Y && y < r.Y + r.Height); + EXPECT_EQ(inOut, inInput) << x << "," << y; + } + } + EXPECT_EQ(Subtract({0, 0, 10, 10}, {0, 0, 10, 10}).size(), 0u); + EXPECT_EQ(Subtract({0, 0, 10, 10}, {20, 20, 5, 5}), (Vector{{0, 0, 10, 10}})); +} + +// The record form: 0 rectangles = all, an empty region = one empty rectangle, and back. +TEST(Damage, PackedRectsRoundTripThroughARecord) { + Int32 out[kMaxRects * 4] = {}; + EXPECT_EQ(PackRects(Region::Full(), out, kMaxRects), 0u); + EXPECT_TRUE(Region::FromEglRects(out, 0).IsFull()); + + Uint32 n = PackRects(Region(), out, kMaxRects); + ASSERT_EQ(n, 1u); + EXPECT_TRUE(Region::FromEglRects(out, static_cast(n)).IsEmpty()) << "nothing damaged stays nothing"; + + const Region two = Of({{1, 2, 3, 4}, {50, 60, 7, 8}}); + n = PackRects(two, out, kMaxRects); + ASSERT_EQ(n, 2u); + EXPECT_EQ(Region::FromEglRects(out, static_cast(n)), two); + + // More than fit: their bounding box, which still covers them. + n = PackRects(two, out, 1); + ASSERT_EQ(n, 1u); + EXPECT_EQ(Region::FromEglRects(out, 1), Of({{1, 2, 56, 66}})); +} + // Three shared images rotate; each write must bring over what that image missed. TEST(Damage, EachBufferCopiesTheDamageItMissedSinceItsLastWrite) { constexpr Int32 W = 200, H = 100; diff --git a/MobileGL/MG_Util/Damage/Damage.h b/MobileGL/MG_Util/Damage/Damage.h index 4d8e379e..10d99678 100644 --- a/MobileGL/MG_Util/Damage/Damage.h +++ b/MobileGL/MG_Util/Damage/Damage.h @@ -80,6 +80,43 @@ namespace MobileGL::MG_Util::Damage { inner.Y + inner.Height <= outer.Y + outer.Height; } + // `a` with `b` taken out: up to four rectangles (the bands above and below `b`, and the parts left + // and right of it in between). + inline Vector Subtract(const Rect& a, const Rect& b) { + const Int32 ax1 = a.X + a.Width, ay1 = a.Y + a.Height; + const Int32 bx1 = b.X + b.Width, by1 = b.Y + b.Height; + if (a.Empty()) return {}; + if (b.Empty() || b.X >= ax1 || bx1 <= a.X || b.Y >= ay1 || by1 <= a.Y) return {a}; + Vector pieces; + const Int32 midY0 = std::max(a.Y, b.Y), midY1 = std::min(ay1, by1); + if (b.Y > a.Y) pieces.push_back({a.X, a.Y, a.Width, b.Y - a.Y}); + if (by1 < ay1) pieces.push_back({a.X, by1, a.Width, ay1 - by1}); + if (b.X > a.X) pieces.push_back({a.X, midY0, b.X - a.X, midY1 - midY0}); + if (bx1 < ax1) pieces.push_back({bx1, midY0, ax1 - bx1, midY1 - midY0}); + return pieces; + } + + // The same pixels as `rects`, as rectangles no two of which overlap (a copy whose regions must + // not overlap in memory takes these). + inline Vector Disjoint(const Vector& rects) { + Vector out; + for (const Rect& rect : rects) { + Vector pieces{rect}; + for (const Rect& taken : out) { + Vector next; + for (const Rect& piece : pieces) { + for (const Rect& left : Subtract(piece, taken)) next.push_back(left); + } + pieces = std::move(next); + if (pieces.empty()) break; + } + for (const Rect& piece : pieces) { + if (!piece.Empty()) out.push_back(piece); + } + } + return out; + } + class Region { public: static Region Full() { @@ -203,6 +240,32 @@ namespace MobileGL::MG_Util::Damage { Vector m_rects; }; + // A region as a record's rectangle list - x, y, width, height per rectangle into `out` (room for + // `maxRects`) - and the count. 0 is the whole surface (what a record that carries no damage + // says); an empty region is one empty rectangle, so it still reads as "nothing" at the other + // end. A region with more rectangles than fit goes as their bounding box. + inline Uint32 PackRects(const Region& region, Int32* out, Uint32 maxRects) { + if (region.IsFull() || maxRects == 0) return 0; + if (region.IsEmpty()) { + out[0] = out[1] = out[2] = out[3] = 0; + return 1; + } + const Vector& rects = region.Rects(); + if (rects.size() > maxRects) { + Rect box; + for (const Rect& rect : rects) box = Bound(box, rect); + out[0] = box.X, out[1] = box.Y, out[2] = box.Width, out[3] = box.Height; + return 1; + } + for (SizeT i = 0; i < rects.size(); ++i) { + out[i * 4 + 0] = rects[i].X; + out[i * 4 + 1] = rects[i].Y; + out[i * 4 + 2] = rects[i].Width; + out[i * 4 + 3] = rects[i].Height; + } + return static_cast(rects.size()); + } + // Per buffer of a rotating set: the damage each has missed since it was last written. class BufferDamageTracker { public: