[video] stabilize guest resources

This commit is contained in:
ParantezTech
2026-07-18 23:02:30 +03:00
parent d81d643227
commit 8fc94bc687
2 changed files with 356 additions and 157 deletions
+10 -7
View File
@@ -36,6 +36,7 @@ public static class PerfOverlay
private static long _presentedInWindow; private static long _presentedInWindow;
private static long _submittedInWindow; private static long _submittedInWindow;
private static long _drawsInWindow; private static long _drawsInWindow;
private static long _guestBufferCacheBytes;
// Refreshed once per second so per-frame fills never allocate. // Refreshed once per second so per-frame fills never allocate.
private static long _statsWindowStart = Stopwatch.GetTimestamp(); private static long _statsWindowStart = Stopwatch.GetTimestamp();
@@ -74,11 +75,8 @@ public static class PerfOverlay
if (last != 0) if (last != 0)
{ {
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency; var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
if (milliseconds < 1000.0) _frameMilliseconds[_frameHistoryIndex] = milliseconds;
{ _frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
}
} }
} }
@@ -88,6 +86,9 @@ public static class PerfOverlay
/// <summary>Called per translated draw/dispatch executed.</summary> /// <summary>Called per translated draw/dispatch executed.</summary>
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow); public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
public static void SetGuestBufferCacheBytes(ulong bytes) =>
Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes));
/// <summary> /// <summary>
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight. /// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
/// Runs on the render thread. /// Runs on the render thread.
@@ -165,7 +166,7 @@ public static class PerfOverlay
Environment.ProcessorCount; Environment.ProcessorCount;
_lastCpuTime = cpuTime; _lastCpuTime = cpuTime;
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0; var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0;
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp); var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
var elapsedSeconds = sessionStart == 0 var elapsedSeconds = sessionStart == 0
? 0L ? 0L
@@ -176,7 +177,9 @@ public static class PerfOverlay
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS"; _line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}"; _line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}"; _line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE"; var heapMb = GC.GetTotalMemory(false) / (1024 * 1024);
var guestBufferMb = Interlocked.Read(ref _guestBufferCacheBytes) / (1024 * 1024);
_line4 = $"MEM {heapMb}M BUF {guestBufferMb}M CPU {_cpuPercent:0}%";
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}"; _line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
} }
} }
+346 -150
View File
@@ -2412,7 +2412,9 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private static bool TryTakeGuestWork(out PendingGuestWork work) private static bool TryTakeGuestWork(
out PendingGuestWork work,
HashSet<string>? excludedQueues = null)
{ {
lock (_gate) lock (_gate)
{ {
@@ -2425,6 +2427,14 @@ internal static unsafe class VulkanVideoPresenter
} }
var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor]; var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor];
if (excludedQueues?.Contains(queueName) == true)
{
_pendingGuestQueueCursor =
(_pendingGuestQueueCursor + 1) % _pendingGuestQueueSchedule.Count;
queuesToProbe--;
continue;
}
if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) || if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) ||
queue.First is not { } first) queue.First is not { } first)
{ {
@@ -2466,6 +2476,32 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private static bool RequeueGuestWorkFront(in PendingGuestWork work)
{
lock (_gate)
{
if (_closed)
{
return false;
}
if (!_pendingGuestWorkByQueue.TryGetValue(work.Queue.Name, out var queue))
{
queue = new LinkedList<PendingGuestWork>();
_pendingGuestWorkByQueue.Add(work.Queue.Name, queue);
_pendingGuestQueueSchedule.Add(work.Queue.Name);
}
// TryTakeGuestWork removes only the item count. Payload ownership
// remains live until CompleteGuestWork, so requeueing must not add
// the retained-byte total a second time.
queue.AddFirst(work);
_pendingGuestWorkCount++;
System.Threading.Monitor.PulseAll(_gate);
return true;
}
}
private static void CompleteGuestWork(in PendingGuestWork pending) private static void CompleteGuestWork(in PendingGuestWork pending)
{ {
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics(); SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
@@ -2875,11 +2911,14 @@ internal static unsafe class VulkanVideoPresenter
DescriptorSetLayout DescriptorSetLayout, DescriptorSetLayout DescriptorSetLayout,
PipelineLayout PipelineLayout); PipelineLayout PipelineLayout);
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length); private readonly record struct DirtyGuestBufferRange(
ulong Offset,
ulong Length,
string QueueName,
ulong Timeline);
private sealed class GuestBufferAllocation private sealed class GuestBufferAllocation
{ {
public string QueueName = VulkanGuestQueueIdentity.Default.Name;
public ulong BaseAddress; public ulong BaseAddress;
public ulong Size; public ulong Size;
public VkBuffer Buffer; public VkBuffer Buffer;
@@ -2890,8 +2929,6 @@ internal static unsafe class VulkanVideoPresenter
public List<DirtyGuestBufferRange> DirtyRanges { get; } = []; public List<DirtyGuestBufferRange> DirtyRanges { get; } = [];
} }
private const string SharedReadOnlyGuestBufferQueue = "shared.readonly";
private sealed class TranslatedDrawResources private sealed class TranslatedDrawResources
{ {
public string DebugName = "SharpEmu translated"; public string DebugName = "SharpEmu translated";
@@ -4795,7 +4832,9 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty( MarkGuestBufferDirty(
allocation, allocation,
globalBuffer.GuestOffset, globalBuffer.GuestOffset,
globalBuffer.GuestSize); globalBuffer.GuestSize,
_activeGuestQueue.Name,
_submitTimeline);
} }
} }
} }
@@ -4991,7 +5030,7 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private void WaitForActiveGuestQueueSubmissionsForCpuVisibility() private bool TryMakeActiveGuestQueueSubmissionsCpuVisible()
{ {
FlushBatchedGuestCommands(); FlushBatchedGuestCommands();
if (!_lastSubmittedTimelineByGuestQueue.TryGetValue( if (!_lastSubmittedTimelineByGuestQueue.TryGetValue(
@@ -4999,7 +5038,7 @@ internal static unsafe class VulkanVideoPresenter
out var targetTimeline) || out var targetTimeline) ||
targetTimeline <= _completedTimeline) targetTimeline <= _completedTimeline)
{ {
return; return true;
} }
PendingGuestSubmission? target = null; PendingGuestSubmission? target = null;
@@ -5019,27 +5058,77 @@ internal static unsafe class VulkanVideoPresenter
$"{targetTimeline} (completed={_completedTimeline})."); $"{targetTimeline} (completed={_completedTimeline}).");
} }
var waitStart = System.Diagnostics.Stopwatch.GetTimestamp();
var fence = target.Fence; var fence = target.Fence;
Check( var status = _vk.GetFenceStatus(_device, fence);
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue), if (status == Result.NotReady)
$"vkWaitForFences(queue visibility: {_activeGuestQueue.Name})"); {
return false;
}
if (status == Result.ErrorDeviceLost)
{
_deviceLost = true;
return true;
}
Check(status, $"vkGetFenceStatus(queue visibility: {_activeGuestQueue.Name})");
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency;
if (_traceVulkanShaderEnabled) if (_traceVulkanShaderEnabled)
{ {
TraceVulkanShader( TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " + $"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " + $"target_timeline={targetTimeline} completed_timeline={_completedTimeline}");
$"waited_ms={waitedMs:F3}");
} }
return true;
} }
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work) private void WaitForGuestBufferAllocationForCpuVisibility(
GuestBufferAllocation allocation)
{ {
WaitForActiveGuestQueueSubmissionsForCpuVisibility(); if (IsGuestBufferAllocationReferencedByOpenBatch(allocation))
{
FlushBatchedGuestCommands();
}
var targetTimeline = allocation.LastUseTimeline;
if (targetTimeline <= _completedTimeline)
{
return;
}
PendingGuestSubmission? target = null;
foreach (var submission in _pendingGuestSubmissions)
{
if (submission.Timeline == targetTimeline)
{
target = submission;
break;
}
}
if (target is null)
{
throw new InvalidOperationException(
$"Guest buffer 0x{allocation.BaseAddress:X16} lost pending timeline " +
$"{targetTimeline} (completed={_completedTimeline}).");
}
var fence = target.Fence;
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
$"vkWaitForFences(buffer visibility: 0x{allocation.BaseAddress:X16})");
CollectCompletedGuestSubmissions(waitForOldest: false);
}
private bool TryExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{
if (!TryMakeActiveGuestQueueSubmissionsCpuVisible())
{
return false;
}
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action(); work.Action();
if (_traceVulkanShaderEnabled) if (_traceVulkanShaderEnabled)
@@ -5049,6 +5138,8 @@ internal static unsafe class VulkanVideoPresenter
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'"); $"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
} }
return true;
} }
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work) private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
@@ -8186,9 +8277,6 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)); var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size); var endAddress = checked(guestBuffer.BaseAddress + size);
GuestBufferAllocation? allocation = null; GuestBufferAllocation? allocation = null;
var allocationPriority = -1;
// This runs for every bound global buffer. Preserve the previous
// stable queue preference without allocating LINQ sort state.
foreach (var candidate in _guestBufferAllocations) foreach (var candidate in _guestBufferAllocations)
{ {
if (candidate.BaseAddress > guestBuffer.BaseAddress || if (candidate.BaseAddress > guestBuffer.BaseAddress ||
@@ -8197,22 +8285,8 @@ internal static unsafe class VulkanVideoPresenter
continue; continue;
} }
var candidatePriority = string.Equals( allocation = candidate;
candidate.QueueName, break;
_activeGuestQueue.Name,
StringComparison.Ordinal)
? 2
: string.Equals(
candidate.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal)
? 1
: 0;
if (candidatePriority > allocationPriority)
{
allocation = candidate;
allocationPriority = candidatePriority;
}
} }
if (allocation is null) if (allocation is null)
@@ -8247,11 +8321,7 @@ internal static unsafe class VulkanVideoPresenter
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length); var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
if (!source.SequenceEqual(shadow)) if (!source.SequenceEqual(shadow))
{ {
var sharedReadOnly = string.Equals( if (!guestBuffer.Writable &&
allocation.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal);
if (sharedReadOnly &&
(allocation.LastUseTimeline > _completedTimeline || (allocation.LastUseTimeline > _completedTimeline ||
IsGuestBufferAllocationReferencedByOpenBatch(allocation))) IsGuestBufferAllocationReferencedByOpenBatch(allocation)))
{ {
@@ -8265,14 +8335,8 @@ internal static unsafe class VulkanVideoPresenter
// an in-flight shader access. Retire prior users, publish their // an in-flight shader access. Retire prior users, publish their
// dirty ranges to guest memory, then upload the current guest // dirty ranges to guest memory, then upload the current guest
// bytes (which may be newer than the parser's captured array). // bytes (which may be newer than the parser's captured array).
if (!sharedReadOnly) WaitForGuestBufferAllocationForCpuVisibility(allocation);
{ WriteBackAllDirtyGuestBuffers();
// Writable aliases are private to one logical guest queue.
// Retiring unrelated queues here recreates the global FIFO
// and turns routine buffer refreshes into queue-wide stalls.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
// Populate the cached shadow copy first and write it out to the // Populate the cached shadow copy first and write it out to the
// mapped allocation in one pass. The mapped memory is // mapped allocation in one pass. The mapped memory is
// HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers), // HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers),
@@ -8418,7 +8482,7 @@ internal static unsafe class VulkanVideoPresenter
return; return;
} }
var ranges = new List<(ulong Start, ulong End, bool Writable)>(buffers.Count); var ranges = new List<(ulong Start, ulong End)>(buffers.Count);
foreach (var buffer in buffers) foreach (var buffer in buffers)
{ {
if (buffer.BaseAddress == 0) if (buffer.BaseAddress == 0)
@@ -8432,8 +8496,7 @@ internal static unsafe class VulkanVideoPresenter
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL; var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
ranges.Add(( ranges.Add((
alignedStart, alignedStart,
paddedEnd, paddedEnd));
buffer.Writable && buffer.WriteBackToGuest));
} }
if (ranges.Count == 0) if (ranges.Count == 0)
@@ -8442,7 +8505,7 @@ internal static unsafe class VulkanVideoPresenter
} }
ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start)); ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start));
var merged = new List<(ulong Start, ulong End, bool Writable)>(ranges.Count); var merged = new List<(ulong Start, ulong End)>(ranges.Count);
foreach (var range in ranges) foreach (var range in ranges)
{ {
if (merged.Count == 0 || range.Start > merged[^1].End) if (merged.Count == 0 || range.Start > merged[^1].End)
@@ -8454,25 +8517,18 @@ internal static unsafe class VulkanVideoPresenter
var previous = merged[^1]; var previous = merged[^1];
merged[^1] = ( merged[^1] = (
previous.Start, previous.Start,
Math.Max(previous.End, range.End), Math.Max(previous.End, range.End));
previous.Writable || range.Writable);
} }
foreach (var range in merged) foreach (var range in merged)
{ {
EnsureGuestBufferAllocation( EnsureGuestBufferAllocation(range.Start, range.End);
range.Start,
range.End,
range.Writable
? _activeGuestQueue.Name
: SharedReadOnlyGuestBufferQueue);
} }
} }
private void EnsureGuestBufferAllocation( private void EnsureGuestBufferAllocation(
ulong requestedStart, ulong requestedStart,
ulong requestedEnd, ulong requestedEnd)
string queueName)
{ {
var start = requestedStart; var start = requestedStart;
var end = requestedEnd; var end = requestedEnd;
@@ -8481,10 +8537,6 @@ internal static unsafe class VulkanVideoPresenter
{ {
overlaps = _guestBufferAllocations overlaps = _guestBufferAllocations
.Where(allocation => .Where(allocation =>
string.Equals(
allocation.QueueName,
queueName,
StringComparison.Ordinal) &&
allocation.BaseAddress < end && allocation.BaseAddress < end &&
start < allocation.BaseAddress + allocation.Size) start < allocation.BaseAddress + allocation.Size)
.ToList(); .ToList();
@@ -8521,7 +8573,7 @@ internal static unsafe class VulkanVideoPresenter
WriteBackAllDirtyGuestBuffers(); WriteBackAllDirtyGuestBuffers();
} }
var replacement = CreateGuestBufferAllocation(start, end, queueName); var replacement = CreateGuestBufferAllocation(start, end);
foreach (var overlap in overlaps) foreach (var overlap in overlaps)
{ {
_guestBufferAllocations.Remove(overlap); _guestBufferAllocations.Remove(overlap);
@@ -8530,22 +8582,27 @@ internal static unsafe class VulkanVideoPresenter
_guestBufferAllocations.Add(replacement); _guestBufferAllocations.Add(replacement);
_guestBufferAllocations.Sort(static (left, right) => _guestBufferAllocations.Sort(static (left, right) =>
{ left.BaseAddress.CompareTo(right.BaseAddress));
var queueOrder = string.CompareOrdinal(left.QueueName, right.QueueName); UpdateGuestBufferCacheMetric();
return queueOrder != 0
? queueOrder
: left.BaseAddress.CompareTo(right.BaseAddress);
});
TraceVulkanShader( TraceVulkanShader(
$"vk.guest_buffer_allocation queue={queueName} " + $"vk.guest_buffer_allocation base=0x{start:X16} bytes={replacement.Size} " +
$"base=0x{start:X16} bytes={replacement.Size} " +
$"merged={overlaps.Count}"); $"merged={overlaps.Count}");
} }
private void UpdateGuestBufferCacheMetric()
{
var bytes = 0UL;
foreach (var allocation in _guestBufferAllocations)
{
bytes = checked(bytes + allocation.Size);
}
PerfOverlay.SetGuestBufferCacheBytes(bytes);
}
private GuestBufferAllocation CreateGuestBufferAllocation( private GuestBufferAllocation CreateGuestBufferAllocation(
ulong start, ulong start,
ulong end, ulong end)
string queueName)
{ {
var size = checked(end - start); var size = checked(end - start);
if (size == 0 || size > int.MaxValue) if (size == 0 || size > int.MaxValue)
@@ -8571,7 +8628,6 @@ internal static unsafe class VulkanVideoPresenter
$"SharpEmu guest VA 0x{start:X16}-0x{end:X16}"); $"SharpEmu guest VA 0x{start:X16}-0x{end:X16}");
return new GuestBufferAllocation return new GuestBufferAllocation
{ {
QueueName = queueName,
BaseAddress = start, BaseAddress = start,
Size = size, Size = size,
Buffer = buffer, Buffer = buffer,
@@ -9577,15 +9633,6 @@ internal static unsafe class VulkanVideoPresenter
MarkSampledImagesInitialized(resources); MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false); MarkStorageImagesInitialized(resources, traceContents: false);
if (work.WritesGlobalMemory)
{
// The CPU submit thread may immediately consume an indirect
// argument written by this dispatch. Wait for the specific
// guest fences and publish only dirty ranges; a queue-wide
// idle unnecessarily serialized presentation work too.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
TraceVulkanShader( TraceVulkanShader(
$"vk.compute_dispatch groups={work.GroupCountX}x" + $"vk.compute_dispatch groups={work.GroupCountX}x" +
$"{work.GroupCountY}x{work.GroupCountZ} " + $"{work.GroupCountY}x{work.GroupCountZ} " +
@@ -9815,7 +9862,9 @@ internal static unsafe class VulkanVideoPresenter
private static void MarkGuestBufferDirty( private static void MarkGuestBufferDirty(
GuestBufferAllocation allocation, GuestBufferAllocation allocation,
ulong offset, ulong offset,
ulong length) ulong length,
string queueName,
ulong timeline)
{ {
if (length == 0) if (length == 0)
{ {
@@ -9827,6 +9876,11 @@ internal static unsafe class VulkanVideoPresenter
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--) for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{ {
var existing = allocation.DirtyRanges[index]; var existing = allocation.DirtyRanges[index];
if (!string.Equals(existing.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
var existingEnd = existing.Offset + existing.Length; var existingEnd = existing.Offset + existing.Length;
if (end < existing.Offset || existingEnd < start) if (end < existing.Offset || existingEnd < start)
{ {
@@ -9835,10 +9889,12 @@ internal static unsafe class VulkanVideoPresenter
start = Math.Min(start, existing.Offset); start = Math.Min(start, existing.Offset);
end = Math.Max(end, existingEnd); end = Math.Max(end, existingEnd);
timeline = Math.Max(timeline, existing.Timeline);
allocation.DirtyRanges.RemoveAt(index); allocation.DirtyRanges.RemoveAt(index);
} }
allocation.DirtyRanges.Add(new DirtyGuestBufferRange(start, end - start)); allocation.DirtyRanges.Add(
new DirtyGuestBufferRange(start, end - start, queueName, timeline));
} }
private void WriteBackAllDirtyGuestBuffers(string? queueName = null) private void WriteBackAllDirtyGuestBuffers(string? queueName = null)
@@ -9851,33 +9907,51 @@ internal static unsafe class VulkanVideoPresenter
foreach (var allocation in _guestBufferAllocations) foreach (var allocation in _guestBufferAllocations)
{ {
if (queueName is not null &&
!string.Equals(allocation.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
if (allocation.DirtyRanges.Count != 0 &&
allocation.LastUseTimeline > _completedTimeline)
{
// A mapped HOST_COHERENT allocation still cannot be read
// by the CPU while a shader may be writing it. Callers
// normally retire the relevant fences first; keep this
// helper fail-closed if a future path forgets to do so.
TraceVulkanShader(
$"vk.global_writeback_deferred base=0x{allocation.BaseAddress:X16} " +
$"last_use={allocation.LastUseTimeline} completed={_completedTimeline}");
continue;
}
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--) for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{ {
var range = allocation.DirtyRanges[index]; var range = allocation.DirtyRanges[index];
if ((queueName is not null &&
!string.Equals(range.QueueName, queueName, StringComparison.Ordinal)) ||
range.Timeline > _completedTimeline)
{
continue;
}
if (range.Length == 0 || range.Length > int.MaxValue) if (range.Length == 0 || range.Length > int.MaxValue)
{ {
continue; continue;
} }
var rangeEnd = checked(range.Offset + range.Length);
var overlapsInFlightWrite = false;
for (var otherIndex = 0;
otherIndex < allocation.DirtyRanges.Count;
otherIndex++)
{
if (otherIndex == index)
{
continue;
}
var other = allocation.DirtyRanges[otherIndex];
if (other.Timeline <= _completedTimeline)
{
continue;
}
var otherEnd = checked(other.Offset + other.Length);
if (range.Offset < otherEnd && other.Offset < rangeEnd)
{
overlapsInFlightWrite = true;
break;
}
}
if (overlapsInFlightWrite)
{
continue;
}
var mappedBytes = new ReadOnlySpan<byte>( var mappedBytes = new ReadOnlySpan<byte>(
(void*)(allocation.Mapped + checked((nint)range.Offset)), (void*)(allocation.Mapped + checked((nint)range.Offset)),
checked((int)range.Length)); checked((int)range.Length));
@@ -9907,6 +9981,7 @@ internal static unsafe class VulkanVideoPresenter
const int pageSize = 4096; const int pageSize = 4096;
const int unreadableMergeGap = 16; const int unreadableMergeGap = 16;
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize); var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
var pageRuns = new List<(int Start, int Length)>(64); var pageRuns = new List<(int Start, int Length)>(64);
try try
{ {
@@ -9915,35 +9990,49 @@ internal static unsafe class VulkanVideoPresenter
pageStart += pageSize) pageStart += pageSize)
{ {
var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length); var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length);
pageRuns.Clear(); var pageLength = pageEnd - pageStart;
var cursor = pageStart; var mappedPageSource = mappedBytes.Slice(pageStart, pageLength);
while (cursor < pageEnd) var shadowPage = shadowBytes.Slice(pageStart, pageLength);
if (mappedPageSource.SequenceEqual(shadowPage))
{ {
while (cursor < pageEnd && continue;
mappedBytes[cursor] == shadowBytes[cursor]) }
// HOST_COHERENT mappings are commonly uncached or
// write-combined on the CPU. Read each changed page
// once with a bulk copy, then perform the byte-level
// merge against ordinary cached memory.
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
mappedPageSource.CopyTo(mappedPage);
pageRuns.Clear();
var cursor = 0;
while (cursor < pageLength)
{
while (cursor < pageLength &&
mappedPage[cursor] == shadowPage[cursor])
{ {
cursor++; cursor++;
} }
if (cursor == pageEnd) if (cursor == pageLength)
{ {
break; break;
} }
var runStart = cursor; var runStart = cursor;
while (cursor < pageEnd && while (cursor < pageLength &&
mappedBytes[cursor] != shadowBytes[cursor]) mappedPage[cursor] != shadowPage[cursor])
{ {
cursor++; cursor++;
} }
var runLength = cursor - runStart; var runLength = cursor - runStart;
pageRuns.Add((runStart, runLength)); pageRuns.Add((pageStart + runStart, runLength));
changedRuns++; changedRuns++;
changedBytes += (ulong)runLength; changedBytes += (ulong)runLength;
if (firstChangedOffset < 0) if (firstChangedOffset < 0)
{ {
firstChangedOffset = runStart; firstChangedOffset = pageStart + runStart;
} }
} }
@@ -9953,13 +10042,12 @@ internal static unsafe class VulkanVideoPresenter
} }
changedPages++; changedPages++;
var pageLength = pageEnd - pageStart;
var livePage = livePageBuffer.AsSpan(0, pageLength); var livePage = livePageBuffer.AsSpan(0, pageLength);
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage)) if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
{ {
foreach (var run in pageRuns) foreach (var run in pageRuns)
{ {
mappedBytes.Slice(run.Start, run.Length).CopyTo( mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
livePage.Slice(run.Start - pageStart, run.Length)); livePage.Slice(run.Start - pageStart, run.Length));
} }
@@ -9967,7 +10055,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
foreach (var run in pageRuns) foreach (var run in pageRuns)
{ {
mappedBytes.Slice(run.Start, run.Length).CopyTo( mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length)); shadowBytes.Slice(run.Start, run.Length));
} }
@@ -9982,7 +10070,9 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty( MarkGuestBufferDirty(
allocation, allocation,
range.Offset + (ulong)run.Start, range.Offset + (ulong)run.Start,
(ulong)run.Length); (ulong)run.Length,
range.QueueName,
range.Timeline);
} }
continue; continue;
@@ -10018,7 +10108,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++) overlayIndex++)
{ {
var run = pageRuns[overlayIndex]; var run = pageRuns[overlayIndex];
mappedBytes.Slice(run.Start, run.Length).CopyTo( mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mergedLive.Slice( mergedLive.Slice(
run.Start - mergedStart, run.Start - mergedStart,
run.Length)); run.Length));
@@ -10034,7 +10124,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++) overlayIndex++)
{ {
var run = pageRuns[overlayIndex]; var run = pageRuns[overlayIndex];
mappedBytes.Slice(run.Start, run.Length).CopyTo( mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length)); shadowBytes.Slice(run.Start, run.Length));
} }
@@ -10051,7 +10141,9 @@ internal static unsafe class VulkanVideoPresenter
exactIndex++) exactIndex++)
{ {
var run = pageRuns[exactIndex]; var run = pageRuns[exactIndex];
var changed = mappedBytes.Slice(run.Start, run.Length); var changed = mappedPage.Slice(
run.Start - pageStart,
run.Length);
fallbackWrites++; fallbackWrites++;
if (memory.TryWrite( if (memory.TryWrite(
guestAddress + (ulong)run.Start, guestAddress + (ulong)run.Start,
@@ -10068,7 +10160,9 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty( MarkGuestBufferDirty(
allocation, allocation,
range.Offset + (ulong)run.Start, range.Offset + (ulong)run.Start,
(ulong)run.Length); (ulong)run.Length,
range.QueueName,
range.Timeline);
} }
} }
} }
@@ -10077,6 +10171,7 @@ internal static unsafe class VulkanVideoPresenter
finally finally
{ {
GuestDataPool.Shared.Return(livePageBuffer); GuestDataPool.Shared.Return(livePageBuffer);
GuestDataPool.Shared.Return(mappedPageBuffer);
} }
var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)]; var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)];
@@ -10346,6 +10441,7 @@ internal static unsafe class VulkanVideoPresenter
} }
GuestDepthResource? depth = null; GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null; DepthFramebufferResource? depthFramebuffer = null;
var clearDepthSeparately = false;
if (ShouldAttachGuestDepth( if (ShouldAttachGuestDepth(
work.DepthTarget, work.DepthTarget,
draw.RenderState.Depth) && draw.RenderState.Depth) &&
@@ -10373,12 +10469,26 @@ internal static unsafe class VulkanVideoPresenter
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth; depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth; depth.ClearDepth = effectiveDepthTarget.ClearDepth;
} }
if (targets.Length == 1) clearDepthSeparately = clearDepthForDraw &&
(depth.Width < firstTarget.Width ||
depth.Height < firstTarget.Height);
if (targets.Length == 1 && !clearDepthSeparately)
{ {
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth); depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
} }
} }
if (depth is not null && !clearDepthSeparately)
{
// Guest color images may be allocated at their maximum
// resolution while the active viewport and DB surface use
// a smaller dynamic-rendering extent. Vulkan requires the
// framebuffer extent to fit every attachment.
extent = new Extent2D(
Math.Min(firstTarget.Width, depth.Width),
Math.Min(firstTarget.Height, depth.Height));
}
if (clearDepthForDraw) if (clearDepthForDraw)
{ {
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB // DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
@@ -10412,17 +10522,18 @@ internal static unsafe class VulkanVideoPresenter
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer; var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
if (targets.Length > 1) if (targets.Length > 1)
{ {
var attachedDepth = clearDepthSeparately ? null : depth;
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer( (renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
formats, formats,
targets.Select(target => target.MipViews.Length > 0 targets.Select(target => target.MipViews.Length > 0
? target.MipViews[0] ? target.MipViews[0]
: target.View).ToArray(), : target.View).ToArray(),
firstTarget.Width, extent.Width,
firstTarget.Height, extent.Height,
targets.Select(target => targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(), target.Initialized || target.InitialUploadPending).ToArray(),
depth, attachedDepth,
depth?.Initialized == true && !clearDepthForDraw); attachedDepth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass; transientRenderPass = renderPass;
transientFramebuffer = framebuffer; transientFramebuffer = framebuffer;
} }
@@ -10433,8 +10544,8 @@ internal static unsafe class VulkanVideoPresenter
formats, formats,
extent, extent,
targets, targets,
hasDepthAttachment: depth is not null, hasDepthAttachment: depth is not null && !clearDepthSeparately,
feedbackDepth: depth); feedbackDepth: clearDepthSeparately ? null : depth);
resources.TransientRenderPass = transientRenderPass; resources.TransientRenderPass = transientRenderPass;
resources.TransientFramebuffer = transientFramebuffer; resources.TransientFramebuffer = transientFramebuffer;
transientRenderPass = default; transientRenderPass = default;
@@ -10454,6 +10565,10 @@ internal static unsafe class VulkanVideoPresenter
submitted = true; submitted = true;
BeginDebugLabel(_commandBuffer, resources.DebugName); BeginDebugLabel(_commandBuffer, resources.DebugName);
if (clearDepthSeparately && depth is not null)
{
RecordStandaloneGuestDepthClear(depth);
}
var hasStorageImages = false; var hasStorageImages = false;
foreach (var texture in resources.Textures) foreach (var texture in resources.Textures)
{ {
@@ -10517,6 +10632,7 @@ internal static unsafe class VulkanVideoPresenter
toColorAttachments); toColorAttachments);
if (depth is not null && if (depth is not null &&
!clearDepthSeparately &&
depth.Layout == ImageLayout.ShaderReadOnlyOptimal) depth.Layout == ImageLayout.ShaderReadOnlyOptimal)
{ {
var toDepthAttachment = new ImageMemoryBarrier var toDepthAttachment = new ImageMemoryBarrier
@@ -10554,7 +10670,7 @@ internal static unsafe class VulkanVideoPresenter
framebuffer, framebuffer,
extent, extent,
colorAttachmentCount: targets.Length, colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null, hasDepthAttachment: depth is not null && !clearDepthSeparately,
clearDepth: depth?.ClearDepth ?? 1f); clearDepth: depth?.ClearDepth ?? 1f);
RecordTranslatedDrawInPass(resources, extent); RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer); _vk.CmdEndRenderPass(_commandBuffer);
@@ -10610,7 +10726,10 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null) if (depth is not null)
{ {
depth.Initialized = true; depth.Initialized = true;
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal; if (!clearDepthSeparately)
{
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
}
if (clearDepthForDraw) if (clearDepthForDraw)
{ {
depth.InitializationSource = "guest-depth-clear"; depth.InitializationSource = "guest-depth-clear";
@@ -11624,14 +11743,6 @@ internal static unsafe class VulkanVideoPresenter
return existing; return existing;
} }
if (depth.Width < color.Width || depth.Height < color.Height)
{
throw new InvalidOperationException(
$"guest depth 0x{depth.Address:X16} extent {depth.Width}x{depth.Height} " +
$"is smaller than color target 0x{color.Address:X16} " +
$"{color.Width}x{color.Height}");
}
var attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View; var attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View;
var loadRenderPass = CreateDepthRenderPass( var loadRenderPass = CreateDepthRenderPass(
color.Format, color.Format,
@@ -11658,8 +11769,8 @@ internal static unsafe class VulkanVideoPresenter
RenderPass = loadRenderPass, RenderPass = loadRenderPass,
AttachmentCount = 2, AttachmentCount = 2,
PAttachments = attachments, PAttachments = attachments,
Width = color.Width, Width = Math.Min(color.Width, depth.Width),
Height = color.Height, Height = Math.Min(color.Height, depth.Height),
Layers = 1, Layers = 1,
}; };
Check( Check(
@@ -12155,6 +12266,7 @@ internal static unsafe class VulkanVideoPresenter
EvictDirtyCachedTextures(); EvictDirtyCachedTextures();
var completedWork = 0; var completedWork = 0;
HashSet<string>? deferredOrderedQueues = null;
var renderWorkDeadline = _renderWorkBudgetTicks > 0 var renderWorkDeadline = _renderWorkBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks ? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
: long.MaxValue; : long.MaxValue;
@@ -12172,7 +12284,7 @@ internal static unsafe class VulkanVideoPresenter
break; break;
} }
if (!TryTakeGuestWork(out var pendingGuestWork)) if (!TryTakeGuestWork(out var pendingGuestWork, deferredOrderedQueues))
{ {
break; break;
} }
@@ -12199,6 +12311,7 @@ internal static unsafe class VulkanVideoPresenter
_enqueueAsImmediateQueueFollowup = true; _enqueueAsImmediateQueueFollowup = true;
_immediateFollowupTail = null; _immediateFollowupTail = null;
var work = pendingGuestWork.Work; var work = pendingGuestWork.Work;
var deferGuestWork = false;
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics(); var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L; var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
@@ -12226,7 +12339,7 @@ internal static unsafe class VulkanVideoPresenter
ExecuteGuestImageWrite(guestImageWrite); ExecuteGuestImageWrite(guestImageWrite);
break; break;
case VulkanOrderedGuestAction orderedAction: case VulkanOrderedGuestAction orderedAction:
ExecuteOrderedGuestAction(orderedAction); deferGuestWork = !TryExecuteOrderedGuestAction(orderedAction);
break; break;
case VulkanOrderedGuestFlip orderedFlip: case VulkanOrderedGuestFlip orderedFlip:
ExecuteOrderedGuestFlip(orderedFlip); ExecuteOrderedGuestFlip(orderedFlip);
@@ -12238,12 +12351,21 @@ internal static unsafe class VulkanVideoPresenter
} }
finally finally
{ {
CompleteGuestWork(pendingGuestWork); if (!deferGuestWork || !RequeueGuestWorkFront(pendingGuestWork))
{
CompleteGuestWork(pendingGuestWork);
}
_enqueueAsImmediateQueueFollowup = false; _enqueueAsImmediateQueueFollowup = false;
_immediateFollowupTail = null; _immediateFollowupTail = null;
Volatile.Write(ref _executingGuestWorkSequence, 0); Volatile.Write(ref _executingGuestWorkSequence, 0);
} }
if (deferGuestWork)
{
deferredOrderedQueues ??= new HashSet<string>(StringComparer.Ordinal);
deferredOrderedQueues.Add(pendingGuestWork.Queue.Name);
}
if (workStart != 0) if (workStart != 0)
{ {
var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart) var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart)
@@ -13200,6 +13322,79 @@ internal static unsafe class VulkanVideoPresenter
depth.Layout = ImageLayout.ShaderReadOnlyOptimal; depth.Layout = ImageLayout.ShaderReadOnlyOptimal;
} }
private void RecordStandaloneGuestDepthClear(GuestDepthResource depth)
{
var depthRange = new ImageSubresourceRange(
ImageAspectFlags.DepthBit,
0,
1,
0,
1);
var sourceStage = PipelineStageFlags.TopOfPipeBit;
var sourceAccess = AccessFlags.None;
switch (depth.Layout)
{
case ImageLayout.ShaderReadOnlyOptimal:
sourceStage =
PipelineStageFlags.VertexShaderBit |
PipelineStageFlags.FragmentShaderBit |
PipelineStageFlags.ComputeShaderBit;
sourceAccess = AccessFlags.ShaderReadBit;
break;
case ImageLayout.DepthStencilAttachmentOptimal:
sourceStage =
PipelineStageFlags.EarlyFragmentTestsBit |
PipelineStageFlags.LateFragmentTestsBit;
sourceAccess =
AccessFlags.DepthStencilAttachmentReadBit |
AccessFlags.DepthStencilAttachmentWriteBit;
break;
case ImageLayout.TransferDstOptimal:
sourceStage = PipelineStageFlags.TransferBit;
sourceAccess = AccessFlags.TransferWriteBit;
break;
}
if (depth.Layout != ImageLayout.TransferDstOptimal)
{
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = sourceAccess,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = depth.Layout,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = depth.Image,
SubresourceRange = depthRange,
};
_vk.CmdPipelineBarrier(
_commandBuffer,
sourceStage,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
}
var clearValue = new ClearDepthStencilValue(depth.ClearDepth, 0);
_vk.CmdClearDepthStencilImage(
_commandBuffer,
depth.Image,
ImageLayout.TransferDstOptimal,
&clearValue,
1,
&depthRange);
depth.Initialized = true;
depth.Layout = ImageLayout.TransferDstOptimal;
depth.InitializationSource = "guest-depth-clear";
}
private void RecordRenderTargetFeedbackSnapshots( private void RecordRenderTargetFeedbackSnapshots(
TranslatedDrawResources resources, TranslatedDrawResources resources,
PipelineStageFlags shaderStage) PipelineStageFlags shaderStage)
@@ -14965,6 +15160,7 @@ internal static unsafe class VulkanVideoPresenter
DestroyGuestBufferAllocation(allocation); DestroyGuestBufferAllocation(allocation);
} }
_guestBufferAllocations.Clear(); _guestBufferAllocations.Clear();
PerfOverlay.SetGuestBufferCacheBytes(0);
_hostBufferPool.Dispose(); _hostBufferPool.Dispose();
foreach (var guestImage in _guestImages.Values) foreach (var guestImage in _guestImages.Values)
{ {