[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.
This commit is contained in:
Nicola Pomarico
2026-07-21 00:01:28 +02:00
committed by GitHub
parent e01092aa38
commit 0ae785c617
@@ -11097,6 +11097,65 @@ internal static unsafe class VulkanVideoPresenter
target.Initialized = true; 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<uint>)[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) private void UploadGuestImageInitialData(GuestImageResource target, byte[] pixels)
{ {
var guestDataFormat = (target.GuestFormat & 0x8000_0000u) != 0 var guestDataFormat = (target.GuestFormat & 0x8000_0000u) != 0
@@ -11109,7 +11168,19 @@ internal static unsafe class VulkanVideoPresenter
target.Format, target.Format,
target.Width, target.Width,
target.Height); 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( if (_rejectedGuestImageUploads.Add(
(target.Address, uploadPixels.Length, expectedByteCount, target.Format))) (target.Address, uploadPixels.Length, expectedByteCount, target.Format)))
@@ -11183,7 +11254,7 @@ internal static unsafe class VulkanVideoPresenter
var copyRegion = new BufferImageCopy var copyRegion = new BufferImageCopy
{ {
BufferOffset = 0, BufferOffset = 0,
BufferRowLength = 0, BufferRowLength = uploadRowLengthTexels,
BufferImageHeight = 0, BufferImageHeight = 0,
ImageSubresource = new ImageSubresourceLayers( ImageSubresource = new ImageSubresourceLayers(
ImageAspectFlags.ColorBit, ImageAspectFlags.ColorBit,