[Feature] (Present): buffer age and swap damage end to end on Espryt and Magma

This commit is contained in:
2026-10-02 15:19:39 -04:00
parent 7090e31dee
commit 74919a3bc1
32 changed files with 626 additions and 52 deletions
+10
View File
@@ -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<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
+38 -5
View File
@@ -11,6 +11,7 @@
#include "MG_State/GLState/TextureState/TextureEnum.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeHandles.h>
#include <MG_Util/Damage/Damage.h>
#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);
@@ -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);
}
@@ -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
+103 -7
View File
@@ -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<SurfaceEntry> 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<std::mutex> lock(g_nativeRegistryMutex);
const auto* entry = FindSessionSurfaceLocked(session, surface);
if (entry == nullptr) return 0;
window = entry->Window != static_cast<NativeWindowType>(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<EGLint>(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<std::mutex> 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<Uint32>(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<GLint>(view.Height), static_cast<GLint>(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<GLint>(view.Height),
static_cast<GLint>(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<GLint>(view.Height), static_cast<GLint>(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<GLenum>(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; }
+5 -1
View File
@@ -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
@@ -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<std::recursive_mutex> lock(m_eglStateMutex);
@@ -577,13 +578,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<unsigned long long>(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<std::recursive_mutex> 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<std::recursive_mutex> lock(m_eglStateMutex);
@@ -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,
@@ -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;
}
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include <MG_Util/Damage/Damage.h>
#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<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> 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
@@ -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
@@ -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<const void> owner;
Uint64 lastSubmit = 0;
// A copy has written the image through this import: what it holds is a frame.
Bool written = false;
};
UnorderedMap<Uint64, SharedImagePresentTarget> m_sharedImagePresentTargets;
void DestroySharedImagePresentTargets(Bool onlyDead);
@@ -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<Int32>(view.Width), static_cast<Int32>(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<MG_Util::Damage::Rect> 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<Int32>(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<VkImageCopy> 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<Uint32>(row.Width), static_cast<Uint32>(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<Uint32>(regions.size()), regions.data());
} else if (!rows.empty()) {
Vector<VkImageBlit> 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<Uint32>(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;
}
+57 -3
View File
@@ -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<std::recursive_mutex> 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<std::recursive_mutex> 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.
+1
View File
@@ -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);
@@ -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;
@@ -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);
@@ -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<void*>(eglCreateImageKHR)},
{"eglDestroyImageKHR", reinterpret_cast<void*>(eglDestroyImageKHR)},
{"eglQueryDmaBufFormatsEXT", reinterpret_cast<void*>(eglQueryDmaBufFormatsEXT)},
{"eglSwapBuffersWithDamageKHR", reinterpret_cast<void*>(eglSwapBuffersWithDamageKHR)},
{"eglSwapBuffersWithDamageEXT", reinterpret_cast<void*>(eglSwapBuffersWithDamageEXT)},
{"eglQueryDmaBufModifiersEXT", reinterpret_cast<void*>(eglQueryDmaBufModifiersEXT)},
{"eglCreatePlatformWindowSurface", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
{"eglCreatePlatformPixmapSurface", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
+37 -10
View File
@@ -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<SizeT>(&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<SizeT>(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<Uint8*>(buffer->map),
static_cast<SizeT>(buffer->stride));
impl.Commit(buffer->buffer);
impl.Commit(buffer->buffer, MG_Util::Damage::Region::Full());
buffer->busy = true;
return true;
}
+7 -1
View File
@@ -34,6 +34,7 @@
#if MOBILEGL_WAYLAND_WINDOWS
#include <EGL/egl.h>
#include <MG_Util/Damage/Damage.h>
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 -
+6
View File
@@ -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);
+28 -3
View File
@@ -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);
+2 -2
View File
@@ -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.
@@ -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)},
@@ -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<EGLint>(reply.BufferAge, 0);
return true;
}
Bool BackendObject_Remote::AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
@@ -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
+16
View File
@@ -41,6 +41,7 @@
#include <Config.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Util/Damage/Damage.h>
#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
+1
View File
@@ -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);
+20 -1
View File
@@ -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<Int32>(std::min<Uint32>(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;
+4 -2
View File
@@ -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);
+37
View File
@@ -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<Rect> input{{0, 0, 10, 10}, {5, 5, 10, 10}, {2, 2, 3, 3}, {20, 0, 4, 4}, {0, 8, 30, 2}};
const Vector<Rect> 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<Rect>{{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<Int32>(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<Int32>(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;
+63
View File
@@ -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<Rect> 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<Rect> 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<Rect> Disjoint(const Vector<Rect>& rects) {
Vector<Rect> out;
for (const Rect& rect : rects) {
Vector<Rect> pieces{rect};
for (const Rect& taken : out) {
Vector<Rect> 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<Rect> 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<Rect>& 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<Uint32>(rects.size());
}
// Per buffer of a rotating set: the damage each has missed since it was last written.
class BufferDamageTracker {
public: