From 0ae785c617aa0b21aaa6fdd768c011f93262b4c7 Mon Sep 17 00:00:00 2001 From: Nicola Pomarico Date: Tue, 21 Jul 2026 00:01:28 +0200 Subject: [PATCH] [VideoPresenter] Accept padded row pitch in guest image uploads (#475) The guest can hand initial texture data whose rows are padded out to a hardware alignment wider than the image width, so the total byte count exceeds the tightly packed width*height*bpp we compute. The upload path rejected any byte count that did not match exactly, silently dropping these uploads and leaving the texture blank. Recover the real source row length when the byte count is consistent with a common padding alignment (8/16/32/64/128/256 texels) and pass it through as BufferRowLength on the copy, instead of always hardcoding 0. Uploads that do not match a recognised padded layout are still rejected as before. Verified against Dead Cells (PPSA15552): a loading-transition texture upload that previously wedged the title now uploads correctly and the game proceeds past the load screen, running stably past 1M draw calls with no stalls. Dreaming Sarah (tightly packed path) still renders normally, confirming no regression to the non-padded case. --- .../VideoOut/VulkanVideoPresenter.cs | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 9f0ab806..bb8c1c71 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -11097,6 +11097,65 @@ internal static unsafe class VulkanVideoPresenter target.Initialized = true; } + // Returns the source row length in texels when the upload is a linear + // image whose rows are padded to a wider hardware pitch, or 0 when the + // data is an exact tightly packed match or not a recognisable padded + // layout (in which case the caller keeps rejecting it). Only a pitch + // equal to the width rounded up to a common alignment is accepted, so an + // oversized buffer that still carries mip data is left rejected rather + // than mis-copied. + private static uint TryGetPaddedUploadRowLength( + GuestImageResource target, + ulong uploadByteCount, + ulong expectedByteCount) + { + if (expectedByteCount == 0 + || target.Width == 0 + || target.Height == 0 + || uploadByteCount <= expectedByteCount) + { + return 0; + } + + var texelCount = (ulong)target.Width * target.Height; + if (texelCount == 0 || expectedByteCount % texelCount != 0) + { + // Block-compressed or otherwise non-linear: no per-texel pitch. + return 0; + } + + var bytesPerTexel = expectedByteCount / texelCount; + if (bytesPerTexel == 0 || uploadByteCount % target.Height != 0) + { + return 0; + } + + var rowBytes = uploadByteCount / target.Height; + if (rowBytes % bytesPerTexel != 0) + { + return 0; + } + + var rowTexels = rowBytes / bytesPerTexel; + if (rowTexels < target.Width || rowTexels > uint.MaxValue) + { + return 0; + } + + foreach (var alignment in (ReadOnlySpan)[8, 16, 32, 64, 128, 256]) + { + if (AlignUp(target.Width, alignment) == rowTexels) + { + return (uint)rowTexels; + } + } + + return 0; + } + + private static uint AlignUp(uint value, uint alignment) => + (value + alignment - 1) / alignment * alignment; + private void UploadGuestImageInitialData(GuestImageResource target, byte[] pixels) { var guestDataFormat = (target.GuestFormat & 0x8000_0000u) != 0 @@ -11109,7 +11168,19 @@ internal static unsafe class VulkanVideoPresenter target.Format, target.Width, target.Height); - if (expectedByteCount == 0 || (ulong)uploadPixels.Length != expectedByteCount) + + // The guest can hand us linear pixel data whose rows are padded out + // to a hardware pitch wider than the image, so the byte count runs + // past the tightly packed width*height*bpp we compute. Recover the + // real source row length and copy with it instead of dropping the + // upload, which would otherwise leave the texture blank. + var uploadRowLengthTexels = TryGetPaddedUploadRowLength( + target, + (ulong)uploadPixels.Length, + expectedByteCount); + if (expectedByteCount == 0 + || ((ulong)uploadPixels.Length != expectedByteCount + && uploadRowLengthTexels == 0)) { if (_rejectedGuestImageUploads.Add( (target.Address, uploadPixels.Length, expectedByteCount, target.Format))) @@ -11183,7 +11254,7 @@ internal static unsafe class VulkanVideoPresenter var copyRegion = new BufferImageCopy { BufferOffset = 0, - BufferRowLength = 0, + BufferRowLength = uploadRowLengthTexels, BufferImageHeight = 0, ImageSubresource = new ImageSubresourceLayers( ImageAspectFlags.ColorBit,