用于部分传输的 Vulkan VkBufferImageCopy

Vulkan VkBufferImageCopy for partial transfer

简而言之,我的问题是当我尝试根据一组 offset/size 不匹配的脏矩形更新图像时。

所以,让我们展示一下问题。 这是正确渲染的对象:

这来自 Chromium Embedded Framework 并通过更新整个图像得到正确呈现 - 这通常是不必要的,CEF 为您提供了一个已更改且需要更新的矩形列表。

完整副本成功实现:

def copyBuffertoImageRegion(self, topleft, size, fullsize):
    print(topleft, size, fullsize)
    with CmdBuffer(self.interface, True) as cmdbuffers:
        region = VkBufferImageCopy(
            bufferOffset=0,
            bufferRowLength=fullsize[0],
            bufferImageHeight=fullsize[1],
            imageSubresource=self.subresource,
            imageExtent=size,
            imageOffset=topleft
        )
        vkCmdCopyBufferToImage(
            cmdbuffers[0],
            self.staging.buffer,
            self.image,
            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
            1,
            region
        )

这种情况下的打印结果为 (0,0,0) (1920, 1080, 1) (1920, 1080) 对于完整副本,这有效。

但是,一旦我尝试使用脏矩形,我就会打印这些值:

(88, 88, 0) (120, 120, 1) (1920, 1080)
(88, 88, 0) (120, 120, 1) (1920, 1080)
(88, 96, 0) (120, 120, 1) (1920, 1080)
(72, 80, 0) (152, 136, 1) (1920, 1080)
(72, 80, 0) (152, 136, 1) (1920, 1080)
(80, 80, 0) (144, 136, 1) (1920, 1080)
(80, 80, 0) (136, 136, 1) (1920, 1080)
(80, 88, 0) (152, 144, 1) (1920, 1080)
(88, 88, 0) (152, 144, 1) (1920, 1080)

如果我正确理解了 VkBufferImageCopy 命令,应该 正确吗?

脏矩形开始的第一个元组;左上点。第二个元组是矩形的宽度、高度和深度,最后一个元组是图像和缓冲区的完整大小。

但是,它看起来像这样: https://imgur.com/qoggLz0

偏移量是 "wrong" - 图形的原点跳来跳去,我不确定范围。

如有任何帮助,我们将不胜感激。

编辑:可能进一步优化的更多信息:

传入数据的处理方式如下:

def fill(self, pointer, rects):
    rect = self.combine_rects(rects)
    ffi.memmove(self.mappedhostmemory, pointer, self.buffer.size)
    with self.interface.main_lock:
        self.image.transitionImageLayout(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
        self.image.copyBuffertoImageRegion(*rect,self.interface.resolution)
        self.image.transitionImageLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)

指针是这个指针: https://github.com/cztomczak/cefpython/blob/master/api/PaintBuffer.md#getintpointer

并且 mappedhostmemory 是我保持映射的暂存缓冲区的映射内存。

所以,如果我能更多地限制 CEF 指针 -> vulkan 上传,甚至不抓取整个纹理并将其放入缓冲区,而是实际上只抓取脏部分,那就更好了。

有关系吗? combine_rects 获取脏 rect 列表,并将其变大,同时牢记队列系列的粒度。

编辑 2: 多亏了到目前为止的答案,越来越近了(谢谢!),但还没有完全解决: https://imgur.com/a/Q7tAR

它仍然跳来跳去,但至少它不再是数据沙拉了。 这是通过设置

实现的
bufferOffset = (topleft[0]*fullsize[0]+topleft[1])*4

在这种情况下,我不需要确保它是 4 的倍数,因为它(至少在我的计算机上,稍后会为一般情况修复)的图像粒度为 (8,8 ,8).

def combine_rects(self, rects):
    #start rect is *resolution, 0, 0
    left, up, width, height = self.start_rect

    for rect in rects:
        rleft, rup, rwidth, rheight = rect

        left = min(left, rleft)
        up = min(up, rup)
        width = max(width, rwidth)
        height = max(height, rheight)

    if width == self.interface.resolution[0] or height == self.interface.resolution[1]:
        #issue full copy
        return (0, 0, 0), (*self.interface.resolution, 1)

    if self.granularity_important:#granularity != (1,1,1)
        left = (left // self.granularity_x) * self.granularity_x
        up = (up // self.granularity_y) * self.granularity_y

        #safety buffer, as we may remove up to granularity-1 texels
        width += self.granularity_x
        height += self.granularity_y

        width  = width  if width  % self.granularity_x == 0 else width  + self.granularity_x - width  % self.granularity_x
        height = height if height % self.granularity_y == 0 else height + self.granularity_y - height % self.granularity_y

    return (left, up, 0), (width, height, 1)

如果这个函数有错误,把它放在这里。

我觉得你所在的地区有点不一致。 您的缓冲区的几何形状是什么?

如果它总是全尺寸的,而你只从它复制子矩形,那么 bufferOffset 不应该是 0 而是根据 topleft 设置(并向下舍入以满足其他限制)。

这可能是一个常见的错误,所以 Vulkan 规范说:

Note that imageOffset does not affect addressing calculations for buffer memory. Instead, bufferOffset can be used to select the starting address in buffer memory.


或者如果缓冲区实际上只是矩形,那么 bufferRowLengthbufferImageHeight 不应该是 fullsize.

在缓冲区和图像之间进行复制时,您有两组参数。一个描述图像中感兴趣的位置;这些由 VkBufferImageCopy::image* 参数定义。另一个描述缓冲区内感兴趣的位置;这些由 VkBufferImageCopy::buffer* 参数定义。

imageExtent 对两者都很重要,因为它描述了将传输多少数据。它在图像的 space 中这样做,但它也会影响缓冲区内的感兴趣区域。

缓冲区当然不包含图像;它们包含任意数据。因此,您描述缓冲区中数据的方式与您描述图像的方式不同。

在缓冲区中,图像数据是紧密打包的;每个像素直接相邻。每个像素元素都按照其格式定义进行存储。复制区域的缓冲区部分由3个参数定义。

bufferRowLength 是从一行到下一行的像素数。 bufferImageHeight 是从一个纹理层到下一个纹理层的行数。

这些参数允许您从缓冲区中进行子选择。例如,如果您的缓冲区在逻辑上存储了一张 100x100 的图像,而您只想复制前 50x50 像素,您仍然提供 bufferRowLength/bufferImageHeight 个 100x100 的值。

imageExtent 会阻止它复制超过每个维度的第 50 个像素。 imageExtent 确定有多少数据被传输 from/to VkImage to/from 缓冲区。因此,如果您将此设置为 50x50,您将获得所需的内容。

请注意,当我说 "first 50x50" 像素时,我指的是左上角的 50x50。如果你想从顶部复制-右边 50x50,那就有点挑战了。

bufferOffset 允许您指定图像数据开始的字节偏移量。并且因为您可以从图像的范围中单独指定行长度,所以您可以通过提供 50 * 元素大小的 bufferOffset 来实现传输 from/to 右上角的 50x50。缓冲区行 length/height 将与以前相同。

从图像坐标到缓冲区字节地址的映射方程如下:

address of (x,y,z) = region->bufferOffset + (((z * imageHeight) + y) * rowLength + x) * elementSize;

所以,如果你想转移 from/to 缓冲区左下角的 50x50,你可以这样做。将 bufferOffset 设置为:

elementSize * (50 * rowLength)

对于右下角的 50x50,您可以将 bufferOffset 设置为:

elementSize * ((50 * rowLength) + 50)

但是请注意,bufferOffset 必须是 4 的倍数(如果格式不是 depth/stencil,则必须是元素大小的倍数)。所以如果这是 R8 格式,这将不起作用,因为 50 不是 4 的倍数。

这也适用于 3D 图层副本。 bufferImageHeight 指定要跳过多少行才能到达下一层。

所以,要做你感兴趣的事情,你需要以下内容(注意:我不知道 Python,所以我只是在猜测语法):

bufferOffset = VkDeviceSize(((fullsize[0] * topleft.y) + topleft.x) * elementSize)

region = VkBufferImageCopy(
    bufferOffset=bufferOffset ,
    bufferRowLength=fullsize[0],
    bufferImageHeight=fullsize[1],
    imageSubresource=self.subresource,
    imageExtent=size,
    imageOffset=topleft
)

您需要根据相关格式计算 elementSize。此外,上述代码仅适用于 2D 副本;对于 3D 副本,您还需要考虑 topleft.y