Merge #938 (pr/videoout-present-races)

This commit is contained in:
kuba 2026-09-30 00:12:21 +02:00
commit f37b033b68
5 changed files with 170 additions and 27 deletions

View File

@ -1109,6 +1109,11 @@ internal static unsafe partial class VulkanVideoPresenter
private void DestroyGuestImage(GuestImageResource resource)
{
if (TryPoolFlipSnapshot(resource))
{
return;
}
if (resource.Image.Handle != 0)
{
_vk.DestroyImage(_device, resource.Image, null);

View File

@ -599,7 +599,10 @@ internal static unsafe partial class VulkanVideoPresenter
VulkanSynchronization.PipelineBarrier(_vk,
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit, 0, 0, null, 0, null, 1, &toShaderRead);
FlushBatchedGuestCommands();
// The tick of this flush, not the shared field: the presenter thread also
// writes _submitTimeline, so reading it here can name an older tick that is
// already complete and let the blit run before the copy above.
var captureTick = FlushBatchedGuestCommands();
submitted = true;
_guestImageVersions.Add(version, snapshot);
@ -616,7 +619,7 @@ internal static unsafe partial class VulkanVideoPresenter
GuestImageAddress: displayBuffer.Address,
GuestImageVersion: version,
IsHdr: VideoOutExports.IsHdrPixelFormat(displayBuffer.PixelFormat),
RequiredTick: _submitTimeline,
RequiredTick: captureTick,
FlipRequestId: requestId);
_latestPresentation = presentation;
_pendingGuestImagePresentations.Enqueue(presentation);

View File

@ -46,6 +46,8 @@ internal static unsafe partial class VulkanVideoPresenter
{
DestroyGuestImage(deferredVersion.Image);
}
DrainFlipSnapshotPool();
DestroySwapchainResources();
Console.Error.WriteLine(
$"[LOADER][INFO] vk.device_memory live_allocations={_deviceInfo.LiveAllocations} " +

View File

@ -51,15 +51,18 @@ internal static unsafe partial class VulkanVideoPresenter
}
// Submits the current recording buffer with everything the batch lists own.
private void FlushBatchedGuestCommands()
// Returns the timeline tick that contains the commands this call submitted, so a
// caller that must wait for its own recording does not read the shared
// _submitTimeline field, which the presenter thread also writes.
private ulong FlushBatchedGuestCommands()
{
if (!_batchOpen)
{
return;
return _submitTimeline;
}
using var profile = RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.QueueSubmit);
_scheduler.Flush();
return _scheduler.Flush();
}
private void PrepareGuestSubmission(SubmitBundle bundle)

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];
@ -709,8 +739,108 @@ internal static unsafe partial class VulkanVideoPresenter
return destination;
}
// Captures the display surface through the store in queue order; presentation reads the copy.
// A flip snapshot is created and retired for every flip; on macOS that is a
// vkCreateImage plus a vkAllocateMemory (and a Metal texture) about twenty
// times a second for the same 1920x1080 target. Keep the retired ones and
// hand them back: a snapshot is fully overwritten by the copy that follows
// (its barrier takes it from Undefined), so its old contents and layout do
// not matter.
// The command-stream thread takes snapshots and the render thread retires
// them, so the pool is guarded.
private readonly Dictionary<(Format Format, uint Width, uint Height), Stack<GuestImageResource>>
_flipSnapshotPool = new();
private readonly object _flipSnapshotPoolGate = new();
private int _pooledFlipSnapshots;
// SHARPEMU_FLIP_SNAPSHOT_POOL=0 turns the pool off; it is on otherwise.
private bool _flipSnapshotPoolOpen =
!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_FLIP_SNAPSHOT_POOL"),
"0",
StringComparison.Ordinal);
private const int MaxPooledFlipSnapshots = 8;
private bool TryTakePooledFlipSnapshot(
Format format,
uint width,
uint height,
ulong address,
long version,
out GuestImageResource snapshot)
{
lock (_flipSnapshotPoolGate)
{
snapshot = null!;
if (!_flipSnapshotPoolOpen ||
!_flipSnapshotPool.TryGetValue((format, width, height), out var available) ||
available.Count == 0)
{
return false;
}
snapshot = available.Pop();
_pooledFlipSnapshots--;
}
snapshot.Address = address;
snapshot.FlipVersion = version;
return true;
}
private bool TryPoolFlipSnapshot(GuestImageResource resource)
{
lock (_flipSnapshotPoolGate)
{
if (!_flipSnapshotPoolOpen || resource.Image.Handle == 0 || resource.Memory.Handle == 0 ||
_pooledFlipSnapshots >= MaxPooledFlipSnapshots)
{
return false;
}
var key = (resource.Format, resource.Width, resource.Height);
if (!_flipSnapshotPool.TryGetValue(key, out var available))
{
available = new Stack<GuestImageResource>();
_flipSnapshotPool[key] = available;
}
resource.Address = 0;
resource.FlipVersion = 0;
available.Push(resource);
_pooledFlipSnapshots++;
return true;
}
}
// Closes the pool and hands every kept snapshot back for a real destroy.
private void DrainFlipSnapshotPool()
{
List<GuestImageResource> kept = [];
lock (_flipSnapshotPoolGate)
{
_flipSnapshotPoolOpen = false;
foreach (var available in _flipSnapshotPool.Values)
{
kept.AddRange(available);
}
_flipSnapshotPool.Clear();
_pooledFlipSnapshots = 0;
}
foreach (var resource in kept)
{
DestroyGuestImage(resource);
}
}
private GuestImageResource CreateGuestFlipSnapshot(Format format, uint width, uint height, ulong address, long version)
{
if (TryTakePooledFlipSnapshot(format, width, height, address, version, out var pooled))
{
SetDebugName(ObjectType.Image, pooled.Image.Handle, $"guest flip v{version} source 0x{address:X16}");
return pooled;
}
var imageInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,