-
Notifications
You must be signed in to change notification settings - Fork 344
Description
Introduction
The copies with compressed texture formats are quite different from other non-compressed formats because the copy unit of the textures in compressed formats should be in texel blocks rather than in texels. In this report we will investigate how to support texture copies with compressed texture formats in WebGPU on the basis of our implementation in Project Dawn.
Parameters
Texture Size
D3D requires the width and height of the textures in block-compressed formats must be multiple of 4.
For a mipmap of a texture whose initial dimensions are divisible by 4, but subdivided levels are not, D3D uses “Physical Size” to refer to the size with paddings to round up to the nearest factor of 4 when texture memory is allocated.
D3D uses “Virtual Size” to refer to the size used in texture sampling. When the texture is sampled, the memory padding is ignored.
GPUBufferCopyView.offset
The buffer offset is required to be a multiple of the compressed texel block size in bytes in Vulkan.
GPUBufferCopyView.rowPitch
The rowPitch in D3D12 and Metal refers to the number of bytes from the start of one row of blocks to the start of the next row of blocks. For example, for Block Compressed formats, one row pitch covers up to 4 scan lines at once.
In Vulkan, the VkBufferImageCopy->bufferRowLength is in texels and the buffer and image addressing algorithm operates on the whole blocks and follows the formula in Vulkan SPEC.
If we use the concept rowPitch in D3D12 and Metal that refers to “the number of bytes from the start of one row of blocks to the start of the next row of blocks”, then we can get the formula to compute bufferRowLength from rowPitch:
In the formula of buffer and image addressing provided by Vulkan SPEC:
rowLength = region->bufferRowLength / blockWidth;Apply the formula with x = z = region->bufferOffset = 0, then we get:
address of (0, y, 0) = y * rowLength * blockSizeInBytes
= y * region->bufferRowLength / blockWidth * blockSizeInBytes;Where y ranges from 0 to region->imageExtent.height/blockHeight.
Apply the definition of rowPitch, then we can calculate bufferRowLength from rowPitch in the following formula:
rowPitch = region->bufferRowLength / blockWidth * blockSizeInBytes
=> region->bufferRowLength = rowPitch / blockSizeInBytes * blockWidthGPUBufferCopyView.imageHeight
Vulkan requires the imageHeight to be a multiple of compressed block height in texels.
GPUTextureCopyView.origin3D
D3D12, Metal and Vulkan all requires Origin3D to be a multiple of the corresponding dimensions of the compressed texel block.
CopySize
CopySize specifies the texture region the data will copy into. It is possible to do copies in the texture subresources which aren't made of whole blocks, for example a 60x60 texture will have mip level 2 be 15x15 texels which isn't a multiple of whole blocks. This is an issue for us to specify copies and causes some diverging validation in native APIs.
In D3D12, the document mentions "in summary, be careful to use aligned memory blocks when copying regions that contain block-compressed data. To do this in an application that gets a memory pointer, make sure that the pointer uses the surface pitch to account for the physical memory size.", which I understand that D3D12 always validates both buffer-texture and texture-texture copies with the physical size.
- For example, if we want to copy data into the
mipmapLevel == 2of a 60x60 texture in BC5 formats from(0, 0), we should specify the size of thepSrcBoxto be 16x16, or we will get the following error message from D3D12 validation layer:
D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: The coordinates in pSrcBox are not aligned properly for the format used with the source. When the format is BC5_TYPELESS, left & right must be a multiple of 4 and top & bottom must be a multiple of 4.
In Metal there are different rules on CopySize between buffer-texture copies and texture-texture copies.
For buffer-texture copies, the document mentions “if the texture’s pixel format is a compressed format, the sourceSize must be a multiple of the pixel format’s block size or be clamped to the edge of the texture if the block extends outside the bounds of a texture”, which means Metal does validations with the virtual size in buffer-texture copies.
- For the case mentioned before, if we are copying from a buffer, we should specify the
sourceSizeto be 15x15, or we will get the following error messages from Metal validation layer:
failed assertion `(destinationOrigin.x + sourceSize.width)(16) must be <= width(15).
For texture-texture copies, the document mentions “for compressed pixel formats, align the copy region to that pixel format's block size”, which means Metal does validations with the physical size in texture-texture copies.
- For the case mentioned before, if we are copying from a texture, I think we should specify the
sourceSizeto be 16x16. However I don’t get any error if I use 15x15 in this case. But if we use 14x14, we will getsize.width(14) must be a multiple of blockWidth(4)
and if we use 17x17 we will get(sourceOrigin.x + adjustedSourceSize.width) (17) must be <= paddedWidth (16).
For Vulkan, the SPEC mentions “If the calling command’s VkImage parameter is a compressed image, imageExtent.width must be a multiple of the compressed texel block width or imageExtent.width + imageOffset.x must equal the image subresource width, and imageExtent.height must be a multiple of the compressed texel block height or imageExtent.height + imageOffset.y must equal the image subresource height” for both buffer-texture copies and texture-texture copies, which indicates Vulkan does all copy validations with the virtual size of the texture subresource.
- For the case mentioned before, we should specify the extent to be 15x15, or we will get the following error message from Vulkan validation layer:
vkCmdCopyImage: Source pRegion[0] with mipLevel [ 2 ], offset [ 0, 0, 0 ], extent [ 16, 16, 1 ] exceeds the source image dimensions. The spec valid usage text states 'The source region specified by each element of pRegions must be a region that is contained within srcImage.
Note that as Vulkan always validates texture copies with virtual size, we find an issue in Vulkan SPEC about the texture-to-texture copies with compressed formats with the parameter extent fitting in one subresource but not fitting in another.
- For example, in the case mentioned above, when we are copying from a 60x60 texture, we will not be able to choose a valid
extentbecause:
extent == (16, 16, 1)is against thepRegions must be a region that is contained within dstImagerule.
extent == (15, 15, 1)is against theextent.width must be a multiple of the compressed texel block width or (extent.width + srcOffset.x) must equal the source image subresource widthrule.
Proposal
Based on previous investigation, our proposal on the copies with compressed texture formats is as follows. Note that our proposal is also the current implementation in Project Dawn with a good set of tests that show these constraints are sufficient and work on all backends.
Texture Size
The width and height of a texture in compressed formats must be multiple of the compressed texel block width and height as is required in D3D12.
GPUBufferCopyView.offset
The buffer offset must be a multiple of the size of compressed texel block in bytes as is required in Vulkan.
GPUBufferCopyView.rowPitch
The concept of rowPitch should be the number of bytes from the start of one row of blocks to the start of the next row of blocks, which is the same in D3D12 and Metal.
GPUBufferCopyView.imageHeight
The imageHeight must be a multiple of compressed texel block height as is required in Vulkan
GPUTextureCopyView.origin3D
The Origin3D must be a multiple of the corresponding dimensions of the compressed texel block as is required in D3D12, Metal and Vulkan.
CopySize
We suggest in WebGPU we should validate copySize with the physical size of the subresource in both buffer-texture and texture-texture copies, which is exactly the same method in D3D12. The main reason is that if we use the virtual size, we will meet the same problem in the texture-texture copies as the one in Vulkan.
Buffer Size
The (bufferSize - GPUBufferCopyView.offset) must be big enough to fill in the physical size of the copy region specified by copySize as is required in D3D12.