When uploading block compressed textures, D3D12 wants w and h of SDL_GPUTextureRegion to be a multiple of the block size (4×4 for BCn formats). Vulkan wants w and h to match the logical dimensions. At sizes 2x2 1x1 (like the last mips in a chain) different logic is required depending on whether D3D12 or Vulkan is used.
It would be nice if the D3D12 backend automatically rounded up the region dimensions under the hood for block-compressed formats so users can universally pass the logical dimensions, or note the differing requirements in the SDL_UploadToGPUTexture wiki.
#define SHADER_FORMAT SDL_GPU_SHADERFORMAT_DXIL
// #define SHADER_FORMAT SDL_GPU_SHADERFORMAT_SPIRV
#define USE_LOGICAL_REGION true
// #define USE_LOGICAL_REGION false
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
static const Uint32 MIP_WIDTHS[] = { 4, 2, 1 };
static const Uint32 MIP_HEIGHTS[] = { 4, 2, 1 };
static const Uint32 MIP_COUNT = 3;
static const Uint32 BLOCK_SIZE = 16; // bytes per BC7 block
static const Uint32 BLOCK_DIM = 4; // texels per block
static const Uint8 MIP_DATA[3 * 16] = { // mip chain 4x4 -> 2x2 -> 1x1. 3 blocks
0x40, 0xf8, 0x6d, 0xd2, 0x7a, 0xa5, 0x4e, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0xf8, 0x6d, 0xd2, 0x7a, 0xa5, 0x4e, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0xf8, 0x6d, 0xd2, 0x7a, 0xa5, 0x4e, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
static void upload_mip(
SDL_GPUDevice* device,
SDL_GPUTexture* texture,
Uint32 mip_level,
Uint32 mip_width,
Uint32 mip_height,
const Uint8* src) {
Uint32 row_width_in_blocks = (mip_width + BLOCK_DIM - 1) / BLOCK_DIM;
Uint32 row_height_in_blocks = (mip_height + BLOCK_DIM - 1) / BLOCK_DIM;
Uint32 unaligned_pitch = row_width_in_blocks * BLOCK_SIZE;
Uint32 aligned_pitch = (unaligned_pitch + 255) & ~(Uint32)255;
Uint32 transfer_size = aligned_pitch * row_height_in_blocks;
SDL_GPUTransferBuffer* tbuf = SDL_CreateGPUTransferBuffer(
device,
&(SDL_GPUTransferBufferCreateInfo){ .size = transfer_size,
.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD });
Uint8* dst = SDL_MapGPUTransferBuffer(device, tbuf, false);
for (Uint32 row = 0; row < row_height_in_blocks; row++) {
SDL_memcpy(dst + row * aligned_pitch, src + row * unaligned_pitch, unaligned_pitch);
}
SDL_UnmapGPUTransferBuffer(device, tbuf);
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
SDL_GPUCopyPass* pass = SDL_BeginGPUCopyPass(cmd);
SDL_GPUTextureTransferInfo info = {
.transfer_buffer = tbuf,
.offset = 0,
.pixels_per_row = (aligned_pitch / BLOCK_SIZE) * BLOCK_DIM,
.rows_per_layer = row_height_in_blocks * BLOCK_DIM,
};
Uint32 region_w = USE_LOGICAL_REGION ? mip_width : row_width_in_blocks * BLOCK_DIM;
Uint32 region_h = USE_LOGICAL_REGION ? mip_height : row_height_in_blocks * BLOCK_DIM;
SDL_GPUTextureRegion region = {
.texture = texture,
.mip_level = mip_level,
.w = region_w,
.h = region_h,
.d = 1,
};
SDL_LogInfo(
SDL_LOG_CATEGORY_APPLICATION,
"mip level %u logical=%ux%u region=%ux%u (%s)\n",
mip_level,
mip_width,
mip_height,
region_w,
region_h,
USE_LOGICAL_REGION ? "logical" : "block-rounded");
SDL_UploadToGPUTexture(pass, &info, ®ion, false);
SDL_EndGPUCopyPass(pass);
if (!SDL_SubmitGPUCommandBuffer(cmd)) {
SDL_LogError(
SDL_LOG_CATEGORY_APPLICATION,
"mip level %u submit failed: %s\n",
mip_level,
SDL_GetError());
} else {
SDL_Log("mip level %u submit succeeded\n", mip_level);
}
SDL_ReleaseGPUTransferBuffer(device, tbuf);
}
int main(int argc, char* argv[]) {
SDL_Init(SDL_INIT_VIDEO);
SDL_SetLogPriorities(SDL_LOG_PRIORITY_TRACE);
SDL_GPUDevice* device = SDL_CreateGPUDevice(SHADER_FORMAT, true, NULL);
SDL_GPUTexture* texture = SDL_CreateGPUTexture(
device,
&(SDL_GPUTextureCreateInfo){
.type = SDL_GPU_TEXTURETYPE_2D,
.format = SDL_GPU_TEXTUREFORMAT_BC7_RGBA_UNORM,
.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
.width = MIP_WIDTHS[0],
.height = MIP_HEIGHTS[0],
.layer_count_or_depth = 1,
.num_levels = MIP_COUNT,
});
const Uint8* src = MIP_DATA;
for (Uint32 mip = 0; mip < MIP_COUNT; mip++) {
upload_mip(device, texture, mip, MIP_WIDTHS[mip], MIP_HEIGHTS[mip], src);
src += BLOCK_SIZE; // each mip is exactly 1 block
}
SDL_ReleaseGPUTexture(device, texture);
SDL_DestroyGPUDevice(device);
SDL_Quit();
return 0;
}
Debug Output for SHADER_FORMAT SDL_GPU_SHADERFORMAT_DXIL USE_LOGICAL_REGION true
SDL_GPU Driver: D3D12
D3D12 Adapter: NVIDIA GeForce GTX 1050 Ti
D3D12 Driver: 32.0.15.6094
Validation layers enabled, expect debug level performance!
WARNING: CheckFeatureSupport for UnrestrictedBufferTextureCopyPitchSupported failed. You may need to provide a vendored D3D12Core.dll through the Agility SDK on older platforms.
mip level 0 logical=4x4 region=4x4 (logical)
mip level 0 submit succeeded
mip level 1 logical=2x2 region=2x2 (logical)
D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: Textures created with certain Formats must align the resource dimensions properly. D3D12_SUBRESOURCE_FOOTPRINT::Format is BC7_UNORM. D3D12_SUBRESOURCE_FOOTPRINT::Width is 2, and must be a multiple of 4. D3D12_SUBRESOURCE_FOOTPRINT::Height is 2, and must be a multiple of 4. [ RESOURCE_MANIPULATION ERROR #867: COPYTEXTUREREGION_INVALIDSRCDIMENSIONS]
WARNING: Texture upload row pitch not aligned to 256 bytes! This is suboptimal on D3D12!
ERROR: Failed to close command list!! Error Code: The parameter is incorrect. (0x80070057)
ERROR: mip level 1 submit failed: Failed to close command list!! Error Code: The parameter is incorrect. (0x80070057)
mip level 2 logical=1x1 region=1x1 (logical)
D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: Textures created with certain Formats must align the resource dimensions properly. D3D12_SUBRESOURCE_FOOTPRINT::Format is BC7_UNORM. D3D12_SUBRESOURCE_FOOTPRINT::Width is 1, and must be a multiple of 4. D3D12_SUBRESOURCE_FOOTPRINT::Height is 1, and must be a multiple of 4. [ RESOURCE_MANIPULATION ERROR #867: COPYTEXTUREREGION_INVALIDSRCDIMENSIONS]
WARNING: Texture upload row pitch not aligned to 256 bytes! This is suboptimal on D3D12!
ERROR: Failed to close command list!! Error Code: The parameter is incorrect. (0x80070057)
ERROR: mip level 2 submit failed: Failed to close command list!! Error Code: The parameter is incorrect. (0x80070057)
Debug Output for SHADER_FORMAT SDL_GPU_SHADERFORMAT_SPIRV USE_LOGICAL_REGION false
SDL_GPU Driver: Vulkan
Vulkan Device: NVIDIA GeForce GTX 1050 Ti
Vulkan Driver: NVIDIA 560.94
Vulkan Conformance: 1.3.8.2
mip level 0 logical=4x4 region=4x4 (block-rounded)
mip level 0 submit succeeded
mip level 1 logical=2x2 region=4x4 (block-rounded)
Validation Error: [ VUID-vkCmdCopyBufferToImage-imageSubresource-07971 ] | MessageID = 0x9a6a1fcf
vkCmdCopyBufferToImage(): pRegions[0].imageOffset.x (0) + extent.width (4) exceeds imageSubresource width extent (2).
The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_BC7_UNORM_BLOCK and an extent of [width = 4, height = 4, depth = 1]
mipLevel 1 is [width = 2, height = 2, depth = 1]
The compressed format block extent (width = 4, height = 4, depth = 1) represents miplevel 1 with a texel block extent [width = 1, height = 1, depth = 1]
Validation Error: [ VUID-vkCmdCopyBufferToImage-imageSubresource-07971 ] | MessageID = 0x9a6a1fcf
vkCmdCopyBufferToImage(): pRegions[0].imageOffset.x (0) + extent.width (4) exceeds imageSubresource width extent (2).
The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_BC7_UNORM_BLOCK and an extent of [width = 4, height = 4, depth = 1]
mipLevel 1 is [width = 2, height = 2, depth = 1]
The compressed format block extent (width = 4, height = 4, depth = 1) represents miplevel 1 with a texel block extent [width = 1, height = 1, depth = 1]
The Vulkan spec states: For each element of pRegions, imageOffset.x and (imageExtent.width + imageOffset.x) must both be greater than or equal to 0 and less than or equal to the width of the specified imageSubresource of dstImage (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdCopyBufferToImage-imageSubresource-07971)
Objects: 3
[0] VkCommandBuffer 0x16406760a70
[1] VkBuffer 0x2a000000002a
[2] VkImage 0x220000000022
The Vulkan spec states: For each element of pRegions, imageOffset.x and (imageExtent.width + imageOffset.x) must both be greater than or equal to 0 and less than or equal to the width of the specified imageSubresource of dstImage (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdCopyBufferToImage-imageSubresource-07971)
Objects: 3
[0] VkCommandBuffer 0x16406760a70
[1] VkBuffer 0x2a000000002a
[2] VkImage 0x220000000022
mip level 1 submit succeeded
mip level 2 logical=1x1 region=4x4 (block-rounded)
Validation Error: [ VUID-vkCmdCopyBufferToImage-imageSubresource-07971 ] | MessageID = 0x9a6a1fcf
vkCmdCopyBufferToImage(): pRegions[0].imageOffset.x (0) + extent.width (4) exceeds imageSubresource width extent (1).
The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_BC7_UNORM_BLOCK and an extent of [width = 4, height = 4, depth = 1]
mipLevel 2 is [width = 1, height = 1, depth = 1]
The compressed format block extent (width = 4, height = 4, depth = 1) represents miplevel 2 with a texel block extent [width = 1, height = 1, depth = 1]
Validation Error: [ VUID-vkCmdCopyBufferToImage-imageSubresource-07971 ] | MessageID = 0x9a6a1fcf
vkCmdCopyBufferToImage(): pRegions[0].imageOffset.x (0) + extent.width (4) exceeds imageSubresource width extent (1).
The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_BC7_UNORM_BLOCK and an extent of [width = 4, height = 4, depth = 1]
mipLevel 2 is [width = 1, height = 1, depth = 1]
The compressed format block extent (width = 4, height = 4, depth = 1) represents miplevel 2 with a texel block extent [width = 1, height = 1, depth = 1]
The Vulkan spec states: For each element of pRegions, imageOffset.x and (imageExtent.width + imageOffset.x) must both be greater than or equal to 0 and less than or equal to the width of the specified imageSubresource of dstImage (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdCopyBufferToImage-imageSubresource-07971)
Objects: 3
[0] VkCommandBuffer 0x164066caf50
[1] VkBuffer 0x2c000000002c
[2] VkImage 0x220000000022
The Vulkan spec states: For each element of pRegions, imageOffset.x and (imageExtent.width + imageOffset.x) must both be greater than or equal to 0 and less than or equal to the width of the specified imageSubresource of dstImage (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdCopyBufferToImage-imageSubresource-07971)
Objects: 3
[0] VkCommandBuffer 0x164066caf50
[1] VkBuffer 0x2c000000002c
[2] VkImage 0x220000000022
mip level 2 submit succeeded
When uploading block compressed textures, D3D12 wants
wandhofSDL_GPUTextureRegionto be a multiple of the block size (4×4 for BCn formats). Vulkan wantswandhto match the logical dimensions. At sizes 2x2 1x1 (like the last mips in a chain) different logic is required depending on whether D3D12 or Vulkan is used.It would be nice if the D3D12 backend automatically rounded up the region dimensions under the hood for block-compressed formats so users can universally pass the logical dimensions, or note the differing requirements in the
SDL_UploadToGPUTexturewiki.Debug Output for
SHADER_FORMAT SDL_GPU_SHADERFORMAT_DXILUSE_LOGICAL_REGION trueDebug Output for
SHADER_FORMAT SDL_GPU_SHADERFORMAT_SPIRVUSE_LOGICAL_REGION false