From 8669b0f782cf944ceb51a437bcdad95178b40d16 Mon Sep 17 00:00:00 2001
From: Laszlo Agocs <laszlo.agocs@qt.io>
Date: Wed, 5 Dec 2018 10:40:30 +0100
Subject: [PATCH] d3d, vk, gl: Add resolve

color only
---
 .../rhi/msaarenderbuffer/msaarenderbuffer.cpp |   6 +-
 src/rhi/qrhi.cpp                              |  17 ++
 src/rhi/qrhi.h                                | 102 ++++++----
 src/rhi/qrhi_p.h                              |  12 ++
 src/rhi/qrhid3d11.cpp                         |  73 ++++++-
 src/rhi/qrhid3d11_p.h                         |  13 +-
 src/rhi/qrhigles2.cpp                         |  49 +++++
 src/rhi/qrhigles2_p.h                         |  12 +-
 src/rhi/qrhimetal.mm                          |   5 +
 src/rhi/qrhimetal_p.h                         |   1 +
 src/rhi/qrhivulkan.cpp                        | 192 +++++++++++++-----
 src/rhi/qrhivulkan_p.h                        |   6 +
 todo.txt                                      |  14 +-
 13 files changed, 392 insertions(+), 110 deletions(-)

diff --git a/examples/rhi/msaarenderbuffer/msaarenderbuffer.cpp b/examples/rhi/msaarenderbuffer/msaarenderbuffer.cpp
index cecfe2c..6c3c982 100644
--- a/examples/rhi/msaarenderbuffer/msaarenderbuffer.cpp
+++ b/examples/rhi/msaarenderbuffer/msaarenderbuffer.cpp
@@ -235,7 +235,11 @@ void Window::customRender()
     cb->setViewport({ 0, 0, float(d.rb->pixelSize().width()), float(d.rb->pixelSize().height()) });
     cb->setVertexInput(0, { { d.vbuf, sizeof(vertexData) } });
     cb->draw(3);
-    cb->endPass();
+
+    // add the resolve (msaa renderbuffer -> non-msaa texture)
+    u = m_r->nextResourceUpdateBatch();
+    u->resolveRenderBuffer(d.tex, d.rb);
+    cb->endPass(u);
 
     // onscreen (quad)
     const QSize outputSizeInPixels = m_sc->effectivePixelSize();
diff --git a/src/rhi/qrhi.cpp b/src/rhi/qrhi.cpp
index b26d710..c62ca59 100644
--- a/src/rhi/qrhi.cpp
+++ b/src/rhi/qrhi.cpp
@@ -464,6 +464,21 @@ void QRhiResourceUpdateBatch::copyTexture(QRhiTexture *dst, QRhiTexture *src)
     d->textureCopies.append({ dst, src, QRhiTextureCopyDescription() });
 }
 
+void QRhiResourceUpdateBatch::resolveTexture(QRhiTexture *dst, const QRhiTextureResolveDescription &desc)
+{
+    d->textureResolves.append({ dst, desc });
+}
+
+void QRhiResourceUpdateBatch::resolveTexture(QRhiTexture *dst, QRhiTexture *src)
+{
+    d->textureResolves.append({ dst, src });
+}
+
+void QRhiResourceUpdateBatch::resolveRenderBuffer(QRhiTexture *dst, QRhiRenderBuffer *src)
+{
+    d->textureResolves.append({ dst, src });
+}
+
 void QRhiResourceUpdateBatch::readBackTexture(const QRhiReadbackDescription &rb, QRhiReadbackResult *result)
 {
     d->textureReadbacks.append({ rb, result });
@@ -511,6 +526,7 @@ void QRhiResourceUpdateBatchPrivate::free()
     staticBufferUploads.clear();
     textureUploads.clear();
     textureCopies.clear();
+    textureResolves.clear();
     textureReadbacks.clear();
     texturePrepares.clear();
 
@@ -524,6 +540,7 @@ void QRhiResourceUpdateBatchPrivate::merge(QRhiResourceUpdateBatchPrivate *other
     staticBufferUploads += other->staticBufferUploads;
     textureUploads += other->textureUploads;
     textureCopies += other->textureCopies;
+    textureResolves += other->textureResolves;
     textureReadbacks += other->textureReadbacks;
     texturePrepares += other->texturePrepares;
 }
diff --git a/src/rhi/qrhi.h b/src/rhi/qrhi.h
index 971af34..d1bbf81 100644
--- a/src/rhi/qrhi.h
+++ b/src/rhi/qrhi.h
@@ -322,6 +322,23 @@ struct Q_RHI_EXPORT QRhiTextureCopyDescription
 
 Q_DECLARE_TYPEINFO(QRhiTextureCopyDescription, Q_MOVABLE_TYPE);
 
+struct Q_RHI_EXPORT QRhiTextureResolveDescription
+{
+    QRhiTextureResolveDescription() { }
+    QRhiTextureResolveDescription(QRhiTexture *src) : sourceTexture(src) { }
+    QRhiTextureResolveDescription(QRhiRenderBuffer *src) : sourceRenderBuffer(src) { }
+
+    // source is either a multisample texture or a multisample renderbuffer
+    QRhiTexture *sourceTexture = nullptr;
+    int sourceLayer = 0;
+    QRhiRenderBuffer *sourceRenderBuffer = nullptr;
+
+    int destinationLayer = 0;
+    int destinationLevel = 0;
+};
+
+Q_DECLARE_TYPEINFO(QRhiTextureResolveDescription, Q_MOVABLE_TYPE);
+
 struct Q_RHI_EXPORT QRhiReadbackDescription
 {
     QRhiReadbackDescription() { } // source is the back buffer of the swapchain of the current frame (if the swapchain supports readback)
@@ -385,45 +402,6 @@ protected:
 
 Q_DECLARE_OPERATORS_FOR_FLAGS(QRhiBuffer::UsageFlags)
 
-class Q_RHI_EXPORT QRhiRenderBuffer : public QRhiResource
-{
-public:
-    enum Type {
-        DepthStencil,
-        Color
-    };
-
-    enum Flag {
-        ToBeUsedWithSwapChainOnly = 1 << 0 // use implicit winsys buffers, don't create anything (GL)
-    };
-    Q_DECLARE_FLAGS(Flags, Flag)
-
-    Type type() const { return m_type; }
-    void setType(Type t) { m_type = t; }
-
-    QSize pixelSize() const { return m_pixelSize; }
-    void setPixelSize(const QSize &sz) { m_pixelSize = sz; }
-
-    int sampleCount() const { return m_sampleCount; }
-    void setSampleCount(int s) { m_sampleCount = s; }
-
-    Flags flags() const { return m_flags; }
-    void setFlags(Flags h) { m_flags = h; }
-
-    virtual bool build() = 0;
-
-protected:
-    QRhiRenderBuffer(QRhiImplementation *rhi, Type type_, const QSize &pixelSize_,
-                     int sampleCount_, Flags flags_);
-    Type m_type;
-    QSize m_pixelSize;
-    int m_sampleCount;
-    Flags m_flags;
-    void *m_reserved;
-};
-
-Q_DECLARE_OPERATORS_FOR_FLAGS(QRhiRenderBuffer::Flags)
-
 class Q_RHI_EXPORT QRhiTexture : public QRhiResource
 {
 public:
@@ -433,7 +411,7 @@ public:
         CubeMap = 1 << 2,
         MipMapped = 1 << 3,
         sRGB = 1 << 4,
-        UsedAsTransferSource = 1 << 5 // will (also) be used as the source of a readback or copy
+        UsedAsTransferSource = 1 << 5 // will (also) be used as the source of a readback or copy or resolve
     };
     Q_DECLARE_FLAGS(Flags, Flag)
 
@@ -552,6 +530,47 @@ protected:
     void *m_reserved;
 };
 
+class Q_RHI_EXPORT QRhiRenderBuffer : public QRhiResource
+{
+public:
+    enum Type {
+        DepthStencil,
+        Color
+    };
+
+    enum Flag {
+        ToBeUsedWithSwapChainOnly = 1 << 0 // use implicit winsys buffers, don't create anything (GL)
+    };
+    Q_DECLARE_FLAGS(Flags, Flag)
+
+    Type type() const { return m_type; }
+    void setType(Type t) { m_type = t; }
+
+    QSize pixelSize() const { return m_pixelSize; }
+    void setPixelSize(const QSize &sz) { m_pixelSize = sz; }
+
+    int sampleCount() const { return m_sampleCount; }
+    void setSampleCount(int s) { m_sampleCount = s; }
+
+    Flags flags() const { return m_flags; }
+    void setFlags(Flags h) { m_flags = h; }
+
+    virtual bool build() = 0;
+
+    virtual QRhiTexture::Format backingFormat() const = 0;
+
+protected:
+    QRhiRenderBuffer(QRhiImplementation *rhi, Type type_, const QSize &pixelSize_,
+                     int sampleCount_, Flags flags_);
+    Type m_type;
+    QSize m_pixelSize;
+    int m_sampleCount;
+    Flags m_flags;
+    void *m_reserved;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(QRhiRenderBuffer::Flags)
+
 class Q_RHI_EXPORT QRhiRenderPassDescriptor : public QRhiResource
 {
 protected:
@@ -1006,6 +1025,9 @@ public:
     void uploadTexture(QRhiTexture *tex, const QImage &image); // shortcut
     void copyTexture(QRhiTexture *dst, QRhiTexture *src, const QRhiTextureCopyDescription &desc);
     void copyTexture(QRhiTexture *dst, QRhiTexture *src); // shortcut
+    void resolveTexture(QRhiTexture *dst, const QRhiTextureResolveDescription &desc);
+    void resolveTexture(QRhiTexture *dst, QRhiTexture *src); // shortcut
+    void resolveRenderBuffer(QRhiTexture *dst, QRhiRenderBuffer *src); // shortcut
     void readBackTexture(const QRhiReadbackDescription &rb, QRhiReadbackResult *result);
 
     // This is not normally needed, textures that have an upload or are used
diff --git a/src/rhi/qrhi_p.h b/src/rhi/qrhi_p.h
index 092b1ab..8177709 100644
--- a/src/rhi/qrhi_p.h
+++ b/src/rhi/qrhi_p.h
@@ -184,6 +184,16 @@ struct QRhiResourceUpdateBatchPrivate
         QRhiTextureCopyDescription desc;
     };
 
+    struct TextureResolve {
+        TextureResolve() { }
+        TextureResolve(QRhiTexture *dst_, const QRhiTextureResolveDescription &desc_)
+            : dst(dst_), desc(desc_)
+        { }
+
+        QRhiTexture *dst = nullptr;
+        QRhiTextureResolveDescription desc;
+    };
+
     struct TextureRead {
         TextureRead() { }
         TextureRead(const QRhiReadbackDescription &rb_, QRhiReadbackResult *result_)
@@ -208,6 +218,7 @@ struct QRhiResourceUpdateBatchPrivate
     QVector<StaticBufferUpload> staticBufferUploads;
     QVector<TextureUpload> textureUploads;
     QVector<TextureCopy> textureCopies;
+    QVector<TextureResolve> textureResolves;
     QVector<TextureRead> textureReadbacks;
     QVector<TexturePrepare> texturePrepares;
 
@@ -225,6 +236,7 @@ Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::DynamicBufferUpdate, Q_MOVABL
 Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::StaticBufferUpload, Q_MOVABLE_TYPE);
 Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::TextureUpload, Q_MOVABLE_TYPE);
 Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::TextureCopy, Q_MOVABLE_TYPE);
+Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::TextureResolve, Q_MOVABLE_TYPE);
 Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::TextureRead, Q_MOVABLE_TYPE);
 Q_DECLARE_TYPEINFO(QRhiResourceUpdateBatchPrivate::TexturePrepare, Q_MOVABLE_TYPE);
 
diff --git a/src/rhi/qrhid3d11.cpp b/src/rhi/qrhid3d11.cpp
index ef8be17..25fa7a6 100644
--- a/src/rhi/qrhid3d11.cpp
+++ b/src/rhi/qrhid3d11.cpp
@@ -821,6 +821,52 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
         cbD->commands.append(cmd);
     }
 
+    for (const QRhiResourceUpdateBatchPrivate::TextureResolve &u : ud->textureResolves) {
+        Q_ASSERT(u.dst);
+        Q_ASSERT(u.desc.sourceTexture || u.desc.sourceRenderBuffer);
+        QD3D11Texture *dstTexD = QRHI_RES(QD3D11Texture, u.dst);
+        if (dstTexD->sampleDesc.Count > 1) {
+            qWarning("Cannot resolve into a multisample texture");
+            continue;
+        }
+        QD3D11Texture *srcTexD = QRHI_RES(QD3D11Texture, u.desc.sourceTexture);
+        QD3D11RenderBuffer *srcRbD = QRHI_RES(QD3D11RenderBuffer, u.desc.sourceRenderBuffer);
+        QD3D11CommandBuffer::Command cmd;
+        cmd.cmd = QD3D11CommandBuffer::Command::ResolveSubRes;
+        cmd.args.resolveSubRes.dst = dstTexD->tex;
+        cmd.args.resolveSubRes.dstSubRes = D3D11CalcSubresource(u.desc.destinationLevel,
+                                                                u.desc.destinationLayer,
+                                                                dstTexD->mipLevelCount);
+        if (srcTexD) {
+            cmd.args.resolveSubRes.src = srcTexD->tex;
+            if (srcTexD->dxgiFormat != dstTexD->dxgiFormat) {
+                qWarning("Resolve source and destination formats do not match");
+                continue;
+            }
+            if (srcTexD->sampleDesc.Count <= 1) {
+                qWarning("Cannot resolve a non-multisample texture");
+                continue;
+            }
+            if (srcTexD->m_pixelSize != dstTexD->m_pixelSize) {
+                qWarning("Resolve source and destination sizes do not match");
+                continue;
+            }
+        } else {
+            cmd.args.resolveSubRes.src = srcRbD->tex;
+            if (srcRbD->dxgiFormat != dstTexD->dxgiFormat) {
+                qWarning("Resolve source and destination formats do not match");
+                continue;
+            }
+            if (srcRbD->m_pixelSize != dstTexD->m_pixelSize) {
+                qWarning("Resolve source and destination sizes do not match");
+                continue;
+            }
+        }
+        cmd.args.resolveSubRes.srcSubRes = D3D11CalcSubresource(0, u.desc.sourceLayer, 1);
+        cmd.args.resolveSubRes.format = dstTexD->dxgiFormat;
+        cbD->commands.append(cmd);
+    }
+
     for (const QRhiResourceUpdateBatchPrivate::TextureRead &u : ud->textureReadbacks) {
         ActiveReadback aRb;
         aRb.desc = u.rb;
@@ -840,7 +886,7 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
                 continue;
             }
             src = texD->tex;
-            dxgiFormat = toD3DTextureFormat(texD->m_format, texD->m_flags);
+            dxgiFormat = texD->dxgiFormat;
             pixelSize = texD->m_pixelSize;
             if (u.rb.level > 0) {
                 pixelSize.setWidth(qFloor(float(qMax(1, pixelSize.width() >> u.rb.level))));
@@ -1288,6 +1334,11 @@ void QRhiD3D11::executeCommandBuffer(QD3D11CommandBuffer *cbD)
                                            cmd.args.copySubRes.src, cmd.args.copySubRes.srcSubRes,
                                            cmd.args.copySubRes.hasSrcBox ? &cmd.args.copySubRes.srcBox : nullptr);
             break;
+        case QD3D11CommandBuffer::Command::ResolveSubRes:
+            context->ResolveSubresource(cmd.args.resolveSubRes.dst, cmd.args.resolveSubRes.dstSubRes,
+                                        cmd.args.resolveSubRes.src, cmd.args.resolveSubRes.srcSubRes,
+                                        cmd.args.resolveSubRes.format);
+            break;
         default:
             break;
         }
@@ -1382,7 +1433,6 @@ bool QD3D11RenderBuffer::build()
 
     QRHI_RES_RHI(QRhiD3D11);
     sampleDesc = rhiD->effectiveSampleCount(m_sampleCount);
-    static const DXGI_FORMAT dsFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
 
     D3D11_TEXTURE2D_DESC desc;
     memset(&desc, 0, sizeof(desc));
@@ -1394,7 +1444,8 @@ bool QD3D11RenderBuffer::build()
     desc.Usage = D3D11_USAGE_DEFAULT;
 
     if (m_type == Color) {
-        desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
+        dxgiFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
+        desc.Format = dxgiFormat;
         desc.BindFlags = D3D11_BIND_RENDER_TARGET;
         HRESULT hr = rhiD->dev->CreateTexture2D(&desc, nullptr, &tex);
         if (FAILED(hr)) {
@@ -1403,8 +1454,7 @@ bool QD3D11RenderBuffer::build()
         }
         D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;
         memset(&rtvDesc, 0, sizeof(rtvDesc));
-        rtvDesc.Format = desc.Format;
-        rtvDesc.ViewDimension = desc.SampleDesc.Count > 1 ? D3D11_RTV_DIMENSION_TEXTURE2DMS
+        rtvDesc.Format = dxgiFormat; rtvDesc.ViewDimension = desc.SampleDesc.Count > 1 ? D3D11_RTV_DIMENSION_TEXTURE2DMS
                                                           : D3D11_RTV_DIMENSION_TEXTURE2D;
         hr = rhiD->dev->CreateRenderTargetView(tex, &rtvDesc, &rtv);
         if (FAILED(hr)) {
@@ -1413,7 +1463,8 @@ bool QD3D11RenderBuffer::build()
         }
         return true;
     } else if (m_type == DepthStencil) {
-        desc.Format = dsFormat;
+        dxgiFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
+        desc.Format = dxgiFormat;
         desc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
         HRESULT hr = rhiD->dev->CreateTexture2D(&desc, nullptr, &tex);
         if (FAILED(hr)) {
@@ -1422,7 +1473,7 @@ bool QD3D11RenderBuffer::build()
         }
         D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
         memset(&dsvDesc, 0, sizeof(dsvDesc));
-        dsvDesc.Format = dsFormat;
+        dsvDesc.Format = dxgiFormat;
         dsvDesc.ViewDimension = desc.SampleDesc.Count > 1 ? D3D11_DSV_DIMENSION_TEXTURE2DMS
                                                           : D3D11_DSV_DIMENSION_TEXTURE2D;
         hr = rhiD->dev->CreateDepthStencilView(tex, &dsvDesc, &dsv);
@@ -1436,6 +1487,11 @@ bool QD3D11RenderBuffer::build()
     return false;
 }
 
+QRhiTexture::Format QD3D11RenderBuffer::backingFormat() const
+{
+    return m_type == Color ? QRhiTexture::RGBA8 : QRhiTexture::UnknownFormat;
+}
+
 QD3D11Texture::QD3D11Texture(QRhiImplementation *rhi, Format format, const QSize &pixelSize,
                              int sampleCount, Flags flags)
     : QRhiTexture(rhi, format, pixelSize, sampleCount, flags)
@@ -1493,6 +1549,7 @@ bool QD3D11Texture::build()
     const bool isCube = m_flags.testFlag(CubeMap);
     const bool hasMipMaps = m_flags.testFlag(MipMapped);
 
+    dxgiFormat = toD3DTextureFormat(m_format, m_flags);
     mipLevelCount = hasMipMaps ? qCeil(log2(qMax(size.width(), size.height()))) + 1 : 1;
     sampleDesc = rhiD->effectiveSampleCount(m_sampleCount);
     if (sampleDesc.Count > 1) {
@@ -1520,7 +1577,7 @@ bool QD3D11Texture::build()
     desc.Height = size.height();
     desc.MipLevels = mipLevelCount;
     desc.ArraySize = isCube ? 6 : 1;;
-    desc.Format = toD3DTextureFormat(m_format, m_flags);
+    desc.Format = dxgiFormat;
     desc.SampleDesc = sampleDesc;
     desc.Usage = D3D11_USAGE_DEFAULT;
     desc.BindFlags = bindFlags;
diff --git a/src/rhi/qrhid3d11_p.h b/src/rhi/qrhid3d11_p.h
index c476678..a6db830 100644
--- a/src/rhi/qrhid3d11_p.h
+++ b/src/rhi/qrhid3d11_p.h
@@ -70,10 +70,12 @@ struct QD3D11RenderBuffer : public QRhiRenderBuffer
                        int sampleCount, QRhiRenderBuffer::Flags flags);
     void release() override;
     bool build() override;
+    QRhiTexture::Format backingFormat() const override;
 
     ID3D11Texture2D *tex = nullptr;
     ID3D11DepthStencilView *dsv = nullptr;
     ID3D11RenderTargetView *rtv = nullptr;
+    DXGI_FORMAT dxgiFormat;
     DXGI_SAMPLE_DESC sampleDesc;
     friend class QRhiD3D11;
 };
@@ -87,6 +89,7 @@ struct QD3D11Texture : public QRhiTexture
 
     ID3D11Texture2D *tex = nullptr;
     ID3D11ShaderResourceView *srv = nullptr;
+    DXGI_FORMAT dxgiFormat;
     uint mipLevelCount = 0;
     DXGI_SAMPLE_DESC sampleDesc;
     uint generation = 0;
@@ -244,7 +247,8 @@ struct QD3D11CommandBuffer : public QRhiCommandBuffer
             Draw,
             DrawIndexed,
             UpdateSubRes,
-            CopySubRes
+            CopySubRes,
+            ResolveSubRes
         };
         enum ClearFlag { Color = 1, Depth = 2, Stencil = 4 };
         Cmd cmd;
@@ -324,6 +328,13 @@ struct QD3D11CommandBuffer : public QRhiCommandBuffer
                 bool hasSrcBox;
                 D3D11_BOX srcBox;
             } copySubRes;
+            struct {
+                ID3D11Resource *dst;
+                UINT dstSubRes;
+                ID3D11Resource *src;
+                UINT srcSubRes;
+                DXGI_FORMAT format;
+            } resolveSubRes;
         } args;
     };
 
diff --git a/src/rhi/qrhigles2.cpp b/src/rhi/qrhigles2.cpp
index 6d67cc6..279acb2 100644
--- a/src/rhi/qrhigles2.cpp
+++ b/src/rhi/qrhigles2.cpp
@@ -702,6 +702,30 @@ void QRhiGles2::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
         cbD->commands.append(cmd);
     }
 
+    for (const QRhiResourceUpdateBatchPrivate::TextureResolve &u : ud->textureResolves) {
+        Q_ASSERT(u.dst);
+        Q_ASSERT(u.desc.sourceTexture || u.desc.sourceRenderBuffer);
+        if (u.desc.sourceTexture) {
+            qWarning("Multisample textures not supported, skipping resolve");
+            continue;
+        }
+        QGles2Texture *dstTexD = QRHI_RES(QGles2Texture, u.dst);
+        QGles2RenderBuffer *srcRbD = QRHI_RES(QGles2RenderBuffer, u.desc.sourceRenderBuffer);
+        if (srcRbD->m_pixelSize != dstTexD->m_pixelSize) {
+            qWarning("Resolve source and destination sizes do not match");
+            continue;
+        }
+        QGles2CommandBuffer::Command cmd;
+        cmd.cmd = QGles2CommandBuffer::Command::BlitFromRenderbuffer;
+        cmd.args.blitFromRb.renderbuffer = srcRbD->renderbuffer;
+        cmd.args.blitFromRb.w = srcRbD->m_pixelSize.width();
+        cmd.args.blitFromRb.h = srcRbD->m_pixelSize.height();
+        cmd.args.blitFromRb.dst = dstTexD;
+        cmd.args.blitFromRb.dstLayer = u.desc.destinationLayer;
+        cmd.args.blitFromRb.dstLevel = u.desc.destinationLevel;
+        cbD->commands.append(cmd);
+    }
+
     for (const QRhiResourceUpdateBatchPrivate::TextureRead &u : ud->textureReadbacks) {
         QGles2CommandBuffer::Command cmd;
         cmd.cmd = QGles2CommandBuffer::Command::ReadPixels;
@@ -1149,6 +1173,26 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
                                          cmd.args.compressedSubImage.glintformat,
                                          cmd.args.compressedSubImage.size, cmd.args.compressedSubImage.data);
             break;
+        case QGles2CommandBuffer::Command::BlitFromRenderbuffer:
+        {
+            GLuint fbo[2];
+            f->glGenFramebuffers(2, fbo);
+            f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]);
+            f->glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                         GL_RENDERBUFFER, cmd.args.blitFromRb.renderbuffer);
+            f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
+            QGles2Texture *texD = cmd.args.blitFromRb.dst;
+            const GLenum targetBase = texD->m_flags.testFlag(QRhiTexture::CubeMap) ? GL_TEXTURE_CUBE_MAP_POSITIVE_X : texD->target;
+            const GLenum target = targetBase + cmd.args.blitFromRb.dstLayer;
+            f->glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target,
+                                      texD->texture, cmd.args.blitFromRb.dstLevel);
+            f->glBlitFramebuffer(0, 0, cmd.args.blitFromRb.w, cmd.args.blitFromRb.h,
+                                 0, 0, cmd.args.blitFromRb.w, cmd.args.blitFromRb.h,
+                                 GL_COLOR_BUFFER_BIT,
+                                 GL_LINEAR);
+            f->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject());
+        }
+            break;
         default:
             break;
         }
@@ -1523,6 +1567,11 @@ bool QGles2RenderBuffer::build()
     return true;
 }
 
+QRhiTexture::Format QGles2RenderBuffer::backingFormat() const
+{
+    return m_type == Color ? QRhiTexture::RGBA8 : QRhiTexture::UnknownFormat;
+}
+
 QGles2Texture::QGles2Texture(QRhiImplementation *rhi, Format format, const QSize &pixelSize,
                              int sampleCount, Flags flags)
     : QRhiTexture(rhi, format, pixelSize, sampleCount, flags)
diff --git a/src/rhi/qrhigles2_p.h b/src/rhi/qrhigles2_p.h
index 86817e5..dd9a2b0 100644
--- a/src/rhi/qrhigles2_p.h
+++ b/src/rhi/qrhigles2_p.h
@@ -78,6 +78,7 @@ struct QGles2RenderBuffer : public QRhiRenderBuffer
                        int sampleCount, QRhiRenderBuffer::Flags flags);
     void release() override;
     bool build() override;
+    QRhiTexture::Format backingFormat() const override;
 
     GLuint renderbuffer = 0;
     int samples;
@@ -240,7 +241,8 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer
             ReadPixels,
             SubImage,
             CompressedImage,
-            CompressedSubImage
+            CompressedSubImage,
+            BlitFromRenderbuffer
         };
         Cmd cmd;
         union {
@@ -351,6 +353,14 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer
                 int size;
                 const void *data; // must come from retainData()
             } compressedSubImage;
+            struct {
+                GLuint renderbuffer;
+                int w;
+                int h;
+                QGles2Texture *dst;
+                int dstLayer;
+                int dstLevel;
+            } blitFromRb;
         } args;
     };
 
diff --git a/src/rhi/qrhimetal.mm b/src/rhi/qrhimetal.mm
index bcf415e..9c0bb90 100644
--- a/src/rhi/qrhimetal.mm
+++ b/src/rhi/qrhimetal.mm
@@ -951,6 +951,11 @@ bool QMetalRenderBuffer::build()
     return true;
 }
 
+QRhiTexture::Format QMetalRenderBuffer::backingFormat() const
+{
+    return m_type == Color ? QRhiTexture::RGBA8 : QRhiTexture::UnknownFormat;
+}
+
 QMetalTexture::QMetalTexture(QRhiImplementation *rhi, Format format, const QSize &pixelSize,
                              int sampleCount, Flags flags)
     : QRhiTexture(rhi, format, pixelSize, sampleCount, flags),
diff --git a/src/rhi/qrhimetal_p.h b/src/rhi/qrhimetal_p.h
index ce89fa6..edd5702 100644
--- a/src/rhi/qrhimetal_p.h
+++ b/src/rhi/qrhimetal_p.h
@@ -75,6 +75,7 @@ struct QMetalRenderBuffer : public QRhiRenderBuffer
     ~QMetalRenderBuffer();
     void release() override;
     bool build() override;
+    QRhiTexture::Format backingFormat() const override;
 
     QMetalRenderBufferData *d;
     uint generation = 0;
diff --git a/src/rhi/qrhivulkan.cpp b/src/rhi/qrhivulkan.cpp
index 3950ca8..7d9aed5 100644
--- a/src/rhi/qrhivulkan.cpp
+++ b/src/rhi/qrhivulkan.cpp
@@ -796,8 +796,7 @@ bool QRhiVulkan::createOffscreenRenderPass(VkRenderPass *rp,
         QVkTexture *texD = QRHI_RES(QVkTexture, colorAttachments[i].texture);
         QVkRenderBuffer *rbD = QRHI_RES(QVkRenderBuffer, colorAttachments[i].renderBuffer);
         Q_ASSERT(texD || rbD);
-        const VkFormat vkformat = texD ? toVkTextureFormat(texD->m_format, texD->m_flags)
-                                       : toVkTextureFormat(rbD->backingTexture->m_format, rbD->backingTexture->m_flags);
+        const VkFormat vkformat = texD ? texD->vkformat : rbD->vkformat;
 
         VkAttachmentDescription attDesc;
         memset(&attDesc, 0, sizeof(attDesc));
@@ -817,8 +816,8 @@ bool QRhiVulkan::createOffscreenRenderPass(VkRenderPass *rp,
 
     const bool hasDepthStencil = depthStencilBuffer || depthTexture;
     if (hasDepthStencil) {
-        const VkFormat dsFormat = depthTexture ? toVkTextureFormat(depthTexture->format(), depthTexture->flags())
-                                               : optimalDepthStencilFormat();
+        const VkFormat dsFormat = depthTexture ? QRHI_RES(QVkTexture, depthTexture)->vkformat
+                                               : QRHI_RES(QVkRenderBuffer, depthStencilBuffer)->vkformat;
         const VkSampleCountFlagBits samples = depthTexture ? QRHI_RES(QVkTexture, depthTexture)->samples
                                                            : QRHI_RES(QVkRenderBuffer, depthStencilBuffer)->samples;
         VkAttachmentDescription attDesc;
@@ -1798,6 +1797,42 @@ void QRhiVulkan::imageBarrier(QRhiCommandBuffer *cb, QRhiTexture *tex,
     texD->layout = newLayout;
 }
 
+void QRhiVulkan::prepareForTransferDest(QRhiCommandBuffer *cb, QVkTexture *texD)
+{
+    if (texD->layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
+        if (texD->layout == VK_IMAGE_LAYOUT_PREINITIALIZED) {
+            imageBarrier(cb, texD,
+                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                         0, VK_ACCESS_TRANSFER_WRITE_BIT,
+                         VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+        } else {
+            imageBarrier(cb, texD,
+                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                         VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
+                         VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+        }
+    }
+}
+
+void QRhiVulkan::finishTransferDest(QRhiCommandBuffer *cb, QVkTexture *texD)
+{
+    imageBarrier(cb, texD,
+                 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+                 VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
+                 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+}
+
+void QRhiVulkan::prepareForTransferSrc(QRhiCommandBuffer *cb, QVkTexture *texD)
+{
+    if (texD->layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
+        Q_ASSERT(texD->m_flags.testFlag(QRhiTexture::UsedAsTransferSource));
+        imageBarrier(cb, texD,
+                     VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                     VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT,
+                     VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+    }
+}
+
 void QRhiVulkan::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates)
 {
     QVkCommandBuffer *cbD = QRHI_RES(QVkCommandBuffer, cb);
@@ -2004,19 +2039,7 @@ void QRhiVulkan::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdat
         vmaUnmapMemory(toVmaAllocator(allocator), a);
         vmaFlushAllocation(toVmaAllocator(allocator), a, 0, stagingSize);
 
-        if (utexD->layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
-            if (utexD->layout == VK_IMAGE_LAYOUT_PREINITIALIZED) {
-                imageBarrier(cb, u.tex,
-                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                             0, VK_ACCESS_TRANSFER_WRITE_BIT,
-                             VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-            } else {
-                imageBarrier(cb, u.tex,
-                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                             VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
-                             VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-            }
-        }
+        prepareForTransferDest(cb, utexD);
 
         df->vkCmdCopyBufferToImage(cbD->cb, utexD->stagingBuffers[currentFrameSlot],
                                    utexD->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
@@ -2034,10 +2057,7 @@ void QRhiVulkan::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdat
             releaseQueue.append(e);
         }
 
-        imageBarrier(cb, u.tex,
-                     VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
-                     VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
-                     VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+        finishTransferDest(cb, utexD);
     }
 
     for (const QRhiResourceUpdateBatchPrivate::TextureCopy &u : ud->textureCopies) {
@@ -2069,27 +2089,8 @@ void QRhiVulkan::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdat
         region.extent.height = size.height();
         region.extent.depth = 1;
 
-        if (srcD->layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
-            Q_ASSERT(srcD->m_flags.testFlag(QRhiTexture::UsedAsTransferSource));
-            imageBarrier(cb, srcD,
-                         VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                         VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT,
-                         VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-        }
-
-        if (dstD->layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
-            if (dstD->layout == VK_IMAGE_LAYOUT_PREINITIALIZED) {
-                imageBarrier(cb, dstD,
-                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                             0, VK_ACCESS_TRANSFER_WRITE_BIT,
-                             VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-            } else {
-                imageBarrier(cb, dstD,
-                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                             VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
-                             VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-            }
-        }
+        prepareForTransferSrc(cb, srcD);
+        prepareForTransferDest(cb, dstD);
 
         df->vkCmdCopyImage(QRHI_RES(QVkCommandBuffer, cb)->cb,
                            srcD->image, srcD->layout,
@@ -2101,10 +2102,76 @@ void QRhiVulkan::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdat
                      VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT,
                      VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
 
-        imageBarrier(cb, dstD,
-                     VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
-                     VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
-                     VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+        finishTransferDest(cb, dstD);
+    }
+
+    for (const QRhiResourceUpdateBatchPrivate::TextureResolve &u : ud->textureResolves) {
+        Q_ASSERT(u.dst);
+        Q_ASSERT(u.desc.sourceTexture || u.desc.sourceRenderBuffer);
+        QVkTexture *dstTexD = QRHI_RES(QVkTexture, u.dst);
+        if (dstTexD->samples > VK_SAMPLE_COUNT_1_BIT) {
+            qWarning("Cannot resolve into a multisample texture");
+            continue;
+        }
+
+        QVkTexture *srcTexD = QRHI_RES(QVkTexture, u.desc.sourceTexture);
+        QVkRenderBuffer *srcRbD = QRHI_RES(QVkRenderBuffer, u.desc.sourceRenderBuffer);
+        VkImage srcImage;
+        VkImageLayout srcLayout;
+        VkImageResolve region;
+        memset(&region, 0, sizeof(region));
+        region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+        region.srcSubresource.baseArrayLayer = u.desc.sourceLayer;
+        region.srcSubresource.layerCount = 1;
+        region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+        region.dstSubresource.mipLevel = u.desc.destinationLevel;
+        region.dstSubresource.baseArrayLayer = u.desc.destinationLayer;
+        region.dstSubresource.layerCount = 1;
+        region.extent.depth = 1;
+
+        if (srcTexD) {
+            if (srcTexD->vkformat != dstTexD->vkformat) {
+                qWarning("Resolve source and destination formats do not match");
+                continue;
+            }
+            if (srcTexD->samples == VK_SAMPLE_COUNT_1_BIT) {
+                qWarning("Cannot resolve a non-multisample texture");
+                continue;
+            }
+            if (srcTexD->m_pixelSize != dstTexD->m_pixelSize) {
+                qWarning("Resolve source and destination sizes do not match");
+                continue;
+            }
+
+            region.extent.width = srcTexD->m_pixelSize.width();
+            region.extent.height = srcTexD->m_pixelSize.height();
+
+            prepareForTransferSrc(cb, srcTexD);
+            srcImage = srcTexD->image;
+            srcLayout = srcTexD->layout;
+        } else {
+            if (srcRbD->vkformat != dstTexD->vkformat) {
+                qWarning("Resolve source and destination formats do not match");
+                continue;
+            }
+            if (srcRbD->m_pixelSize != dstTexD->m_pixelSize) {
+                qWarning("Resolve source and destination sizes do not match");
+                continue;
+            }
+
+            region.extent.width = srcRbD->m_pixelSize.width();
+            region.extent.height = srcRbD->m_pixelSize.height();
+
+            prepareForTransferSrc(cb, srcRbD->backingTexture);
+            srcImage = srcRbD->backingTexture->image;
+            srcLayout = srcRbD->backingTexture->layout;
+        }
+
+        prepareForTransferDest(cb, dstTexD);
+
+        df->vkCmdResolveImage(cbD->cb, srcImage, srcLayout, dstTexD->image, dstTexD->layout, 1, &region);
+
+        finishTransferDest(cb, dstTexD);
     }
 
     for (const QRhiResourceUpdateBatchPrivate::TextureRead &u : ud->textureReadbacks) {
@@ -3134,8 +3201,10 @@ void QVkRenderBuffer::release()
     QRHI_RES_RHI(QRhiVulkan);
     rhiD->releaseQueue.append(e);
 
-    if (backingTexture)
+    if (backingTexture) {
+        Q_ASSERT(backingTexture->lastActiveFrameSlot == -1);
         backingTexture->release();
+    }
 }
 
 bool QVkRenderBuffer::build()
@@ -3150,15 +3219,19 @@ bool QVkRenderBuffer::build()
     case QRhiRenderBuffer::Color:
     {
         if (!backingTexture) {
-            backingTexture = QRHI_RES(QVkTexture, rhiD->createTexture(QRhiTexture::RGBA8, m_pixelSize,
-                                                                      m_sampleCount, QRhiTexture::RenderTarget));
+            backingTexture = QRHI_RES(QVkTexture, rhiD->createTexture(QRhiTexture::RGBA8,
+                                                                      m_pixelSize,
+                                                                      m_sampleCount,
+                                                                      QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
         }
         if (!backingTexture->build())
             return false;
+        vkformat = backingTexture->vkformat;
     }
         break;
     case QRhiRenderBuffer::DepthStencil:
-        if (!rhiD->createTransientImage(rhiD->optimalDepthStencilFormat(),
+        vkformat = rhiD->optimalDepthStencilFormat();
+        if (!rhiD->createTransientImage(vkformat,
                                         m_pixelSize,
                                         VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
                                         VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT,
@@ -3180,6 +3253,11 @@ bool QVkRenderBuffer::build()
     return true;
 }
 
+QRhiTexture::Format QVkRenderBuffer::backingFormat() const
+{
+    return m_type == Color ? QRhiTexture::RGBA8 : QRhiTexture::UnknownFormat;
+}
+
 QVkTexture::QVkTexture(QRhiImplementation *rhi, Format format, const QSize &pixelSize,
                        int sampleCount, Flags flags)
     : QRhiTexture(rhi, format, pixelSize, sampleCount, flags)
@@ -3225,7 +3303,7 @@ bool QVkTexture::build()
         release();
 
     QRHI_RES_RHI(QRhiVulkan);
-    VkFormat vkformat = toVkTextureFormat(m_format, m_flags);
+    vkformat = toVkTextureFormat(m_format, m_flags);
     VkFormatProperties props;
     rhiD->f->vkGetPhysicalDeviceFormatProperties(rhiD->physDev, vkformat, &props);
     const bool canSampleOptimal = (props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT);
@@ -3241,6 +3319,16 @@ bool QVkTexture::build()
 
     mipLevelCount = hasMipMaps ? qCeil(log2(qMax(size.width(), size.height()))) + 1 : 1;
     samples = rhiD->effectiveSampleCount(m_sampleCount);
+    if (samples > VK_SAMPLE_COUNT_1_BIT) {
+        if (isCube) {
+            qWarning("Cubemap texture cannot be multisample");
+            return false;
+        }
+        if (hasMipMaps) {
+            qWarning("Multisample texture cannot have mipmaps");
+            return false;
+        }
+    }
 
     VkImageCreateInfo imageInfo;
     memset(&imageInfo, 0, sizeof(imageInfo));
@@ -3482,7 +3570,7 @@ bool QVkTextureRenderTarget::build()
                     viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
                     viewInfo.image = texD->image;
                     viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
-                    viewInfo.format = toVkTextureFormat(texD->format(), texD->flags());
+                    viewInfo.format = texD->vkformat;
                     viewInfo.components.r = VK_COMPONENT_SWIZZLE_R;
                     viewInfo.components.g = VK_COMPONENT_SWIZZLE_G;
                     viewInfo.components.b = VK_COMPONENT_SWIZZLE_B;
diff --git a/src/rhi/qrhivulkan_p.h b/src/rhi/qrhivulkan_p.h
index 40d25e9..9aaecb6 100644
--- a/src/rhi/qrhivulkan_p.h
+++ b/src/rhi/qrhivulkan_p.h
@@ -85,12 +85,14 @@ struct QVkRenderBuffer : public QRhiRenderBuffer
     ~QVkRenderBuffer();
     void release() override;
     bool build() override;
+    QRhiTexture::Format backingFormat() const override;
 
     VkDeviceMemory memory = VK_NULL_HANDLE;
     VkImage image = VK_NULL_HANDLE;
     VkImageView imageView = VK_NULL_HANDLE;
     VkSampleCountFlagBits samples;
     QVkTexture *backingTexture = nullptr;
+    VkFormat vkformat;
     int lastActiveFrameSlot = -1;
     friend class QRhiVulkan;
 };
@@ -108,6 +110,7 @@ struct QVkTexture : public QRhiTexture
     VkBuffer stagingBuffers[QVK_FRAMES_IN_FLIGHT];
     QVkAlloc stagingAllocations[QVK_FRAMES_IN_FLIGHT];
     VkImageLayout layout = VK_IMAGE_LAYOUT_PREINITIALIZED;
+    VkFormat vkformat;
     uint mipLevelCount = 0;
     VkSampleCountFlagBits samples;
     int lastActiveFrameSlot = -1;
@@ -411,6 +414,9 @@ public:
     QRhi::FrameOpResult endNonWrapperFrame(QRhiSwapChain *swapChain);
     void prepareNewFrame(QRhiCommandBuffer *cb);
     void prepareFrameEnd();
+    void prepareForTransferDest(QRhiCommandBuffer *cb, QVkTexture *texD);
+    void finishTransferDest(QRhiCommandBuffer *cb, QVkTexture *texD);
+    void prepareForTransferSrc(QRhiCommandBuffer *cb, QVkTexture *texD);
     void enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates);
     void executeBufferHostWritesForCurrentFrame(QVkBuffer *bufD);
     void activateTextureRenderTarget(QRhiCommandBuffer *cb, QRhiTextureRenderTarget *rt);
diff --git a/todo.txt b/todo.txt
index 1cc0656..80e092f 100644
--- a/todo.txt
+++ b/todo.txt
@@ -16,12 +16,9 @@ test cubemap
 test cubemap face as target
 face cubemap readback? (test vk/d3d, impl for gl/mtl)
 
-d3d: resolveimage (color)
-vk: resolveimage (color)
-gl: resolveimage (color)
-
 gl: tex formats (texture, readback)
 gl: srgb
+gl: readback and resolve could be made more optimal by taking a rt as source
 
 mipmap generation?
 
@@ -63,15 +60,18 @@ more tex: 3d, array?
 vk compressed tex: could it consume a complete ktx without any memcpys?
 multi mip/layer copy? (fewer barriers...)
 multi-buffer (region) readback?
-depth readback
-copy image depth
-depth resolve
+depth readback?
+copy image depth?
+depth resolve?
 
 shadertools:
 dxc for d3d as an alternative to fxc?
 hlsl -> dxc -> spirv -> spirv-cross hmmm...
 
 +++ done
+d3d: resolveimage (color)
+vk: resolveimage (color)
+gl: resolveimage (color)
 vk: color renderbuffer
 d3d: color renderbuffer
 gl: color renderbuffer
-- 
GitLab