mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (Present): buffer age and swap damage end to end on Espryt and Magma
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user