From 4512688301985f5cbdbffdcdb784cd933c3b8fa3 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 00:36:14 +0200 Subject: [PATCH] [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. --- .../VulkanVideoPresenter.Present.Frames.cs | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Frames.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Frames.cs index 5a275ec..826e3a7 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Frames.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Frames.cs @@ -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];