[VideoOut] Give every swapchain image its own sRGB-encode intermediate

A float guest flip is blitted through an sRGB image before it is copied into
the UNORM swapchain image, and one such intermediate served every frame. Each
frame discards its contents by transitioning it Undefined -> TransferDst
before blitting, so with two frames in flight the next frame's blit raced the
previous frame's copy-out and the presented image came out fully zeroed. The
guest's own display buffers held the correct picture the whole time, so the
window simply went black for long stretches.

Each swapchain image now owns its own intermediate, created and destroyed with
the swapchain, so a frame can only ever discard the intermediate belonging to
the image it is about to present.

On Superliminal the same run went from 60 of 84 presented-swapchain readbacks
black to 10 of 187, all of them from before the menu appears, and the menu no
longer turns black after a couple of minutes.
This commit is contained in:
kuba 2026-09-27 00:36:14 +02:00
parent 4d65ff6d51
commit 4512688301
1 changed files with 52 additions and 22 deletions

View File

@ -145,11 +145,16 @@ internal static unsafe partial class VulkanVideoPresenter
// for any dim scene. Encode linear->sRGB by blitting through an sRGB
// intermediate (sRGB stores encode), then raw-copying the encoded
// bytes into the same-class UNORM swapchain image.
private Image _presentEncodeImage;
private DeviceMemory _presentEncodeMemory;
// One intermediate per swapchain image: every frame discards the encode
// image's contents (Undefined -> TransferDst) before blitting into it,
// so a single shared image lets the next frame in flight overwrite the
// one the previous frame has not copied out yet, which presents as a
// fully zeroed (black) swapchain image.
private Image[] _presentEncodeImages = [];
private DeviceMemory[] _presentEncodeMemories = [];
private Extent2D _presentEncodeExtent;
private bool TryGetPresentEncodeImage(out Image encodeImage)
private bool TryGetPresentEncodeImage(uint imageIndex, out Image encodeImage)
{
encodeImage = default;
var encodeFormat = GetSrgbCounterpart(_swapchainFormat);
@ -158,14 +163,29 @@ internal static unsafe partial class VulkanVideoPresenter
return false;
}
if (_presentEncodeImage.Handle != 0 &&
if (_presentEncodeImages.Length != 0 &&
(_presentEncodeExtent.Width != _extent.Width ||
_presentEncodeExtent.Height != _extent.Height))
{
DestroyPresentEncodeImage();
}
if (_presentEncodeImage.Handle == 0)
if (_presentEncodeImages.Length <= imageIndex)
{
var grown = new Image[_swapchainImages.Length];
var grownMemory = new DeviceMemory[_swapchainImages.Length];
_presentEncodeImages.CopyTo(grown, 0);
_presentEncodeMemories.CopyTo(grownMemory, 0);
_presentEncodeImages = grown;
_presentEncodeMemories = grownMemory;
}
if (imageIndex >= _presentEncodeImages.Length)
{
return false;
}
if (_presentEncodeImages[imageIndex].Handle == 0)
{
var imageInfo = new ImageCreateInfo
{
@ -183,11 +203,11 @@ internal static unsafe partial class VulkanVideoPresenter
InitialLayout = ImageLayout.Undefined,
};
Check(
_vk.CreateImage(_device, &imageInfo, null, out _presentEncodeImage),
_vk.CreateImage(_device, &imageInfo, null, out _presentEncodeImages[imageIndex]),
"vkCreateImage(present encode)");
_vk.GetImageMemoryRequirements(
_device,
_presentEncodeImage,
_presentEncodeImages[imageIndex],
out var requirements);
var allocationInfo = new MemoryAllocateInfo
{
@ -198,35 +218,45 @@ internal static unsafe partial class VulkanVideoPresenter
MemoryPropertyFlags.DeviceLocalBit),
};
Check(
_deviceInfo.AllocateMemory(allocationInfo, out _presentEncodeMemory),
_deviceInfo.AllocateMemory(allocationInfo, out _presentEncodeMemories[imageIndex]),
"vkAllocateMemory(present encode)");
Check(
_vk.BindImageMemory(_device, _presentEncodeImage, _presentEncodeMemory, 0),
_vk.BindImageMemory(
_device,
_presentEncodeImages[imageIndex],
_presentEncodeMemories[imageIndex],
0),
"vkBindImageMemory(present encode)");
_presentEncodeExtent = new Extent2D(_extent.Width, _extent.Height);
SetDebugName(
ObjectType.Image,
_presentEncodeImage.Handle,
"SharpEmu present sRGB-encode image");
_presentEncodeImages[imageIndex].Handle,
$"SharpEmu present sRGB-encode image {imageIndex}");
}
encodeImage = _presentEncodeImage;
encodeImage = _presentEncodeImages[imageIndex];
return true;
}
private void DestroyPresentEncodeImage()
{
if (_presentEncodeImage.Handle != 0)
for (var index = 0; index < _presentEncodeImages.Length; index++)
{
_vk.DestroyImage(_device, _presentEncodeImage, null);
_presentEncodeImage = default;
if (_presentEncodeImages[index].Handle != 0)
{
_vk.DestroyImage(_device, _presentEncodeImages[index], null);
_presentEncodeImages[index] = default;
}
if (_presentEncodeMemories[index].Handle != 0)
{
_deviceInfo.FreeMemory(_presentEncodeMemories[index]);
_presentEncodeMemories[index] = default;
}
}
if (_presentEncodeMemory.Handle != 0)
{
_deviceInfo.FreeMemory(_presentEncodeMemory);
_presentEncodeMemory = default;
}
_presentEncodeImages = [];
_presentEncodeMemories = [];
_presentEncodeExtent = default;
}
@ -291,7 +321,7 @@ internal static unsafe partial class VulkanVideoPresenter
if (IsLinearFloatPresentSource(source.Format) &&
GetSrgbCounterpart(PresentationTargetFormat) != Format.Undefined)
{
encodeForPresent = TryGetPresentEncodeImage(out encodeImage);
encodeForPresent = TryGetPresentEncodeImage(imageIndex, out encodeImage);
}
var encodeToTransferDst = new ImageMemoryBarrier2
@ -303,7 +333,7 @@ internal static unsafe partial class VulkanVideoPresenter
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _presentEncodeImage,
Image = encodeImage,
SubresourceRange = ColorSubresourceRange(),
};
var barriers = stackalloc ImageMemoryBarrier2[3];