From 027d963bc2d0d38df066b78be27eac00611a32b5 Mon Sep 17 00:00:00 2001 From: Oleksandr Shyshatskyi Date: Sun, 3 May 2026 22:03:22 +0300 Subject: [PATCH] Vulkan: fix block-compressed texture upload MappedTexture::Map and Unmap sized the staging buffer and ImagePlane pitch with (width * height * BytesPerPixel). For block-compressed formats (DXT/BC) the mapping table sets BytesPerPixel = 0, so the multiplication produced a 0-byte staging buffer and Map() also bailed out at the BytesPerPixel == 0 gate. Net effect: every BC upload was silently dropped, the VkImage stayed at VK_IMAGE_LAYOUT_UNDEFINED, and the bitmap sampled as black. Add VkUploadPitch / VkUploadLevelSize helpers that branch on the mapping's BytesPerPixel: - BytesPerPixel != 0 (uncompressed): keep the original width * BytesPerPixel math. This is the *destination* (Vulkan) bytes per pixel, which matters when a format conversion happens in the scanline copy -- e.g. Image_R8G8B8 maps to VK_FORMAT_R8G8B8A8_UNORM via Image_CopyScanline24_Extend_RGB_RGBA, so the staging buffer holds 4 bpp even though the source is 3 bpp. Switching this site to ImageData::GetFormatPitch (which returns *source* pitch) undersizes the staging buffer for these expanding-conversion formats and the scanline copy runs off the end -- crash on embedded JPEG decode. - BytesPerPixel == 0 (compressed): fall back to ImageData::GetMipLevelSize / GetFormatPitch, which already do the correct 4x4 block math for BC/ATC and don't need any conversion (BC scanlines are copied verbatim). Replace the BytesPerPixel == 0 early-exit in Map() with a totalSize == 0 guard so BC formats now pass through. D3D11 doesn't have this issue because it uses USAGE_STAGING textures and trusts D3D11_MAPPED_TEXTURE2D::RowPitch from the driver; Vulkan uses staging *buffers* + vkCmdCopyBufferToImage where buffer layout is the application's responsibility. Reproducer: load any DXT5 .dds via flash.display.Loader.load on the Vulkan backend (e.g. WoWP loading.swf's logoLogIn_*.dds backgrounds). Pre-fix: black background. Post-fix: image renders. Verified: SWFs with embedded 24-bit JPEGs (e.g. login.swf) still load without regression. --- Src/Render/Vulkan/Vulkan_Texture.cpp | 49 ++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/Src/Render/Vulkan/Vulkan_Texture.cpp b/Src/Render/Vulkan/Vulkan_Texture.cpp index 25d996b7..240987d9 100644 --- a/Src/Render/Vulkan/Vulkan_Texture.cpp +++ b/Src/Render/Vulkan/Vulkan_Texture.cpp @@ -16,6 +16,37 @@ Authors : Scaleform Vulkan Backend namespace Scaleform { namespace Render { namespace Vulkan { +// Upload-layout helpers for the staging buffer in MappedTexture::Map/Unmap. +// +// The mapping table's BytesPerPixel is the *destination* (Vulkan) bytes per +// pixel. For uncompressed formats with width/format conversion (e.g. +// Image_R8G8B8 -> VK_FORMAT_R8G8B8A8_UNORM, which uses +// Image_CopyScanline24_Extend_RGB_RGBA), the staging buffer holds 4 bpp, +// not the source's 3 bpp -- so width * BytesPerPixel is correct here and +// ImageData::GetFormatPitch (which returns source-format pitch) would +// undersize the buffer. +// +// For block-compressed formats (DXT/BC/ATC) BytesPerPixel is 0; the source +// and destination layouts are identical (no conversion happens, scanlines +// are copied verbatim), so we fall back to ImageData's format-aware +// helpers, which handle the 4x4 block math correctly. +static inline UPInt VkUploadPitch(const TextureFormat::Mapping* mapping, + ImageFormat fmt, unsigned mipW, + unsigned plane) +{ + if (mapping && mapping->BytesPerPixel != 0) + return (UPInt)mipW * mapping->BytesPerPixel; + return ImageData::GetFormatPitch(fmt, mipW, plane); +} +static inline UPInt VkUploadLevelSize(const TextureFormat::Mapping* mapping, + ImageFormat fmt, unsigned mipW, + unsigned mipH, unsigned plane) +{ + if (mapping && mapping->BytesPerPixel != 0) + return (UPInt)mipW * mipH * mapping->BytesPerPixel; + return ImageData::GetMipLevelSize(fmt, ImageSize(mipW, mipH), plane); +} + // *** Format mapping table static const TextureFormat::Mapping TextureFormatMapping[] = { @@ -449,8 +480,9 @@ bool MappedTexture::Map(Render::Texture* ptexture, unsigned mipLevel, unsigned l levelCount = 1; const TextureFormat::Mapping* mapping = vktex->GetTextureFormatMapping(); - if (!mapping || mapping->BytesPerPixel == 0) + if (!mapping) return false; + const ImageFormat fmt = vktex->GetImageFormat(); unsigned texPlaneCount = vktex->TextureCount; if (texPlaneCount > PlaneReserveSize) @@ -464,9 +496,11 @@ bool MappedTexture::Map(Render::Texture* ptexture, unsigned mipLevel, unsigned l { unsigned mipW = Alg::Max(1u, sz.Width >> level); unsigned mipH = Alg::Max(1u, sz.Height >> level); - totalSize += (UPInt)mipW * mipH * mapping->BytesPerPixel; + totalSize += VkUploadLevelSize(mapping, fmt, mipW, mipH, itex); } } + if (totalSize == 0) + return false; VkBufferCreateInfo bufCI = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; bufCI.size = totalSize; @@ -527,8 +561,8 @@ bool MappedTexture::Map(Render::Texture* ptexture, unsigned mipLevel, unsigned l { unsigned mipW = Alg::Max(1u, sz.Width >> level); unsigned mipH = Alg::Max(1u, sz.Height >> level); - UPInt pitch = (UPInt)mipW * mapping->BytesPerPixel; - UPInt planeSize = pitch * mipH; + UPInt pitch = VkUploadPitch(mapping, fmt, mipW, itex); + UPInt planeSize = VkUploadLevelSize(mapping, fmt, mipW, mipH, itex); Data.SetPlane(planeIdx, ImageSize(mipW, mipH), pitch, planeSize, pdata); pdata += planeSize; @@ -554,6 +588,7 @@ void MappedTexture::Unmap(bool applyUpdate) if (applyUpdate) { const TextureFormat::Mapping* mapping = vktex->GetTextureFormatMapping(); + const ImageFormat fmt = vktex->GetImageFormat(); UPInt totalSize = 0; for (unsigned itex = 0; itex < vktex->TextureCount; itex++) { @@ -562,7 +597,7 @@ void MappedTexture::Unmap(bool applyUpdate) { unsigned mipW = Alg::Max(1u, sz.Width >> level); unsigned mipH = Alg::Max(1u, sz.Height >> level); - totalSize += (UPInt)mipW * mipH * mapping->BytesPerPixel; + totalSize += VkUploadLevelSize(mapping, fmt, mipW, mipH, itex); } } @@ -585,6 +620,7 @@ void MappedTexture::Unmap(bool applyUpdate) if (applyUpdate) { const TextureFormat::Mapping* mapping = vktex->GetTextureFormatMapping(); + const ImageFormat fmt = vktex->GetImageFormat(); VkCommandBufferAllocateInfo cbAI = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbAI.commandPool = cmdPool; @@ -622,7 +658,8 @@ void MappedTexture::Unmap(bool applyUpdate) vkCmdCopyBufferToImage(cmd, pStagingBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - bufferOffset += (VkDeviceSize)mipW * mipH * mapping->BytesPerPixel; + bufferOffset += (VkDeviceSize)VkUploadLevelSize( + mapping, fmt, mipW, mipH, itex); } pmgr->TransitionImageLayout(cmd, image,