[plasma/kwin] src: multigpuswapchain: fix the damage coordinate system

Xaver Hugl <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 66f5d546709b2ebefc27906a8735416ccb619998 by Xaver Hugl.
Committed on 03/08/2026 at 12:01.
Pushed by zamundaaa into branch 'master'.

multigpuswapchain: fix the damage coordinate system

It used OpenGL coordinates, but our coordinate system is flipped on the Y axis
(matching Vulkan). This mismatch could cause weird glitches, since buffers were
only partially copied over, and was only not caught in previous testing because
the drm backend calculated the damage wrong and games always do full damage.

This also enables the OpenGL path to actually clip to the damage at all, since
GLTexture::render by default just completely ignores the damage region.

M  +13   -2    src/backends/drm/drm_egl_layer_surface.cpp
M  +18   -7    src/multigpuswapchain.cpp

https://invent.kde.org/plasma/kwin/-/commit/66f5d546709b2ebefc27906a8735416ccb619998

diff --git a/src/backends/drm/drm_egl_layer_surface.cpp b/src/backends/drm/drm_egl_layer_surface.cpp
index 41c0c2661e0..49c39d2c5ac 100644
--- a/src/backends/drm/drm_egl_layer_surface.cpp
+++ b/src/backends/drm/drm_egl_layer_surface.cpp
@@ -547,8 +547,19 @@ std::shared_ptr<DrmFramebuffer> EglGbmLayerSurface::importWithCopy(Surface *surf
         glFinish();
     }
 
-    const QSize orientedSize = source->texture()->contentTransform().map(source->texture()->size());
-    const Region bufferDamage = source->texture()->contentTransform().map(damagedDeviceRegion, orientedSize);
+    // The source texture transform looks as follows: output transform + flip-y transform. The
+    // flip-y transform is added by the render backend to handle the OpenGL render target origin
+    // being in the bottom-left corner.
+    //
+    // As is, without the flip-y transform, the top-left corner of the graphics buffer will
+    // map to the bottom-left corner of the render target and the final image will look upside down.
+    //
+    // .combine(FlipY) undoes the flip-y transform in "output transform + flip-y" so we only end
+    // with the output transform and the bufferDamage has its origin in the top-left corner rather
+    // than in the bottom-left corner.
+    const OutputTransform mapping = source->texture()->contentTransform().combine(OutputTransform::FlipY);
+    const QSize orientedSize = mapping.map(source->texture()->size());
+    const Region bufferDamage = mapping.map(damagedDeviceRegion, orientedSize);
     auto imported = surface->importSwapchain->copyRgbBuffer(source->buffer(), bufferDamage, std::move(readFence), frame, source->releasePoint());
     if (!imported) {
         // this is probably caused by a GPU reset, let's not take any chances
diff --git a/src/multigpuswapchain.cpp b/src/multigpuswapchain.cpp
index 118d54bcd4e..eda3966339d 100644
--- a/src/multigpuswapchain.cpp
+++ b/src/multigpuswapchain.cpp
@@ -254,8 +254,8 @@ std::optional<MultiGpuSwapchain::Ret> MultiGpuSwapchain::copyWithVulkan(Graphics
                 1,
             },
             std::array{
-                vk::Offset3D{rect.left(), completeRect.height() - rect.bottom(), 0},
-                vk::Offset3D{rect.right(), completeRect.height() - rect.top(), 1},
+                vk::Offset3D{rect.left(), rect.top(), 0},
+                vk::Offset3D{rect.right(), rect.bottom(), 1},
             },
             // dst
             vk::ImageSubresourceLayers{
@@ -265,8 +265,8 @@ std::optional<MultiGpuSwapchain::Ret> MultiGpuSwapchain::copyWithVulkan(Graphics
                 1,
             },
             std::array{
-                vk::Offset3D{rect.left(), completeRect.height() - rect.bottom(), 0},
-                vk::Offset3D{rect.right(), completeRect.height() - rect.top(), 1},
+                vk::Offset3D{rect.left(), rect.top(), 0},
+                vk::Offset3D{rect.right(), rect.bottom(), 1},
             },
         };
     }) | std::ranges::to<std::vector>();
@@ -349,17 +349,28 @@ std::optional<MultiGpuSwapchain::Ret> MultiGpuSwapchain::copyWithEGL(GraphicsBuf
     }
 
     const Rect completeRect{QPoint(), m_size};
-    const Region toRender = (m_journal.accumulate(m_currentEglSlot->age(), completeRect) | damage) & completeRect;
+    // GLVertexBuffer flips the clip region vertically. In other words, it maps (0, 0) to the
+    // top-left corner of the render target. It does so because it's more convenient in the
+    // rendering code.
+    //
+    // However, the input damage region is specified in the final graphics buffer coordinates,
+    // with the origin in the top-left corner. In other words, damage = flipVertically(toRender),
+    // so we apply a flip-y transform to get a toRender region so when GLVertexBuffer flips it,
+    // we get the original input damage region.
+    const Region toRender = OutputTransform(OutputTransform::FlipY).map((m_journal.accumulate(m_currentEglSlot->age(), completeRect) | damage) & completeRect, m_size);
     m_journal.add(damage);
 
     m_copyContext->pushFramebuffer(m_currentEglSlot->framebuffer());
-    // TODO when possible, use a blit instead of a shader for better performance?
     ShaderBinder binder(sourceTex->target() == GL_TEXTURE_EXTERNAL_OES ? ShaderTrait::MapExternalTexture : ShaderTrait::MapTexture);
     QMatrix4x4 proj;
     proj.scale(1, -1);
     proj.ortho(QRectF(QPointF(), buffer->size()));
     binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, proj);
-    sourceTex->render(toRender, buffer->size());
+
+    glEnable(GL_SCISSOR_TEST);
+    sourceTex->render(toRender, buffer->size(), true);
+    glDisable(GL_SCISSOR_TEST);
+
     m_copyContext->popFramebuffer();
     EGLNativeFence fence(m_copyContext->displayObject());
     m_eglSwapchain->release(m_currentEglSlot, fence.fileDescriptor().duplicate());
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.