771 lines
34 KiB
C#
771 lines
34 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
namespace SharpEmu.Libs.VideoOut;
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Agc;
|
|
using SharpEmu.Libs.Gpu;
|
|
using SharpEmu.Libs.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Gpu.Images;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.ShaderCompiler;
|
|
using SharpEmu.Libs.Gpu.Vulkan;
|
|
using Silk.NET.Vulkan;
|
|
|
|
internal static unsafe partial class VulkanVideoPresenter
|
|
{
|
|
// The presenter whose render thread runs the command stream; null while no window runs.
|
|
private static Presenter? _activePresenter;
|
|
private static Exception? _presenterStartupFailure;
|
|
internal static Func<ICpuMemory, AgcExports.HeadlessCommandStream>? TestCommandStreamFactory { get; set; }
|
|
|
|
public static void SubmitCommandStream(ICpuMemory memory, uint queue, ulong address, uint dwordCount, ulong submissionId, object? geometrySnapshots)
|
|
{
|
|
SubmissionFlowProfile.RecordGuest(SubmissionFlowProfile.EventKind.PresenterEntered,
|
|
queue, submissionId, address, dwordCount);
|
|
lock (_gate)
|
|
{
|
|
if (_closed || HostSessionControl.IsShutdownRequested || Volatile.Read(ref _presenterCloseRequested))
|
|
{
|
|
if (_presenterStartupFailure is { } failure)
|
|
throw new InvalidOperationException("The graphics presenter could not start.", failure);
|
|
throw new OperationCanceledException("The graphics session is shutting down.");
|
|
}
|
|
}
|
|
if (TestCommandStreamFactory?.Invoke(memory) is { } testStream)
|
|
{
|
|
testStream.Submit(queue, address, dwordCount, submissionId, geometrySnapshots);
|
|
return;
|
|
}
|
|
|
|
EnsureStarted(1280, 720);
|
|
// Queue publication precedes device initialization; only the render thread consumes it.
|
|
lock (_gate)
|
|
{
|
|
while (_activePresenter is null && !_closed && !HostSessionControl.IsShutdownRequested &&
|
|
!Volatile.Read(ref _presenterCloseRequested))
|
|
{
|
|
System.Threading.Monitor.Wait(_gate);
|
|
}
|
|
|
|
if (_presenterStartupFailure is { } failure)
|
|
{
|
|
throw new InvalidOperationException("The graphics presenter could not start.", failure);
|
|
}
|
|
if (_closed || HostSessionControl.IsShutdownRequested || Volatile.Read(ref _presenterCloseRequested))
|
|
{
|
|
throw new OperationCanceledException("The graphics session is shutting down.");
|
|
}
|
|
_activePresenter!.EnqueueCommandStream(queue, address, dwordCount, submissionId, geometrySnapshots);
|
|
Presenter.WakeRenderThread();
|
|
}
|
|
}
|
|
|
|
public static IdleOutcome SubmitDone(ICpuMemory memory) =>
|
|
Volatile.Read(ref _presenterStartupFailure) is not null ? IdleOutcome.Failed :
|
|
HostSessionControl.IsShutdownRequested || Volatile.Read(ref _closed) || Volatile.Read(ref _presenterCloseRequested) ? IdleOutcome.Cancelled :
|
|
TryGetActivePresenter(out var presenter)
|
|
? presenter.CommandStream.Done()
|
|
: TestCommandStreamFactory?.Invoke(memory) is { } testStream
|
|
? testStream.Done()
|
|
: IdleOutcome.Completed;
|
|
|
|
// The blocked heads of the stream this memory submits to; null when no stream exists for it.
|
|
internal static BlockedSnapshot? SnapshotBlockedCommandStream(ICpuMemory? memory)
|
|
{
|
|
if (TryGetActivePresenter(out var presenter))
|
|
{
|
|
return presenter.CommandStream.SnapshotBlocked();
|
|
}
|
|
|
|
return memory is not null && TestCommandStreamFactory?.Invoke(memory) is { } testStream
|
|
? testStream.Queue.SnapshotBlocked()
|
|
: null;
|
|
}
|
|
|
|
private static bool TryGetActivePresenter([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Presenter? presenter)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
presenter = _closed ? null : _activePresenter;
|
|
return presenter is not null;
|
|
}
|
|
}
|
|
|
|
// A draw or dispatch reaches the presenter only from the render thread; none is queued.
|
|
private static bool TryGetRenderThreadPresenter([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Presenter? presenter)
|
|
{
|
|
presenter = Volatile.Read(ref _activePresenter);
|
|
if (presenter is null || !presenter.IsVulkanReady)
|
|
{
|
|
presenter = null;
|
|
return false;
|
|
}
|
|
|
|
if (!presenter.Relay.IsGpuQueueThread)
|
|
{
|
|
throw SubmissionScheduler.Fatal("A guest draw was submitted from a thread other than the render thread.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// A video-out export flip: queued on the graphics queue, so it captures after the draws before it.
|
|
public static bool TrySubmitGuestImage(int videoOutHandle, int displayBufferIndex, ulong address, uint width, uint height, uint pitchInPixel, ulong flipRequestId)
|
|
{
|
|
_ = width;
|
|
_ = height;
|
|
_ = pitchInPixel;
|
|
if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!presenter.CommandStream.TryEnqueueFlipPreparation(videoOutHandle, displayBufferIndex, flipRequestId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Presenter.WakeRenderThread();
|
|
return true;
|
|
}
|
|
|
|
private static readonly string[] _commandQueueNames = CreateCommandQueueNames();
|
|
|
|
private static string[] CreateCommandQueueNames()
|
|
{
|
|
var names = new string[CommandStreamQueue.QueueCount];
|
|
names[0] = "dcb.graphics";
|
|
for (var queueId = 1; queueId < names.Length; queueId++)
|
|
{
|
|
names[queueId] = $"acb.compute[{GpuCommandInterpreter.ComputeQueueBase + queueId - 1}]";
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
private sealed partial class Presenter : ICommandStreamHost, IFlipSubmitter
|
|
{
|
|
// This partial hosts the command interpreter on the render thread.
|
|
|
|
private readonly CommandStreamQueue _commandStream;
|
|
private readonly EndOfPipeEvents _endOfPipeEvents = new();
|
|
private EndOfPipe _endOfPipe = null!;
|
|
private AgcExports.CommandStreamTranslation _translation = null!;
|
|
private ICpuMemory _guestMemory = null!;
|
|
private long _lastCommandStreamProgressTicks;
|
|
private static readonly long _blockedRetryTicks =
|
|
CommandStreamQueue.AllBlockedRetryMilliseconds * System.Diagnostics.Stopwatch.Frequency / 1000L;
|
|
|
|
public CommandStreamQueue CommandStream => _commandStream;
|
|
|
|
private void CreateCommandStream()
|
|
{
|
|
var (_, guest, _) = RequireGuestMemory("command stream");
|
|
_guestMemory = guest;
|
|
_endOfPipe = new EndOfPipe(_endOfPipeEvents, this);
|
|
_translation = AgcExports.CreateCommandStreamTranslation(guest, this);
|
|
_lastCommandStreamProgressTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
}
|
|
|
|
public void EnqueueCommandStream(uint queue, ulong address, uint dwordCount, ulong submissionId, object? geometrySnapshots)
|
|
{
|
|
RenderPhaseProfile.RecordSubmissionArrival();
|
|
if (queue == 0)
|
|
{
|
|
_commandStream.EnqueueGraphics(address, dwordCount, submissionId, geometrySnapshots);
|
|
}
|
|
else
|
|
{
|
|
_commandStream.EnqueueCompute(queue, address, dwordCount, submissionId, geometrySnapshots);
|
|
}
|
|
}
|
|
|
|
// Runs slices until the budget ends or nothing is runnable; blocked heads retry every 100 ms.
|
|
private void RunCommandStreamSlices(long renderWorkDeadline)
|
|
{
|
|
var slices = 0;
|
|
while (slices < _maxGuestWorkPerRender)
|
|
{
|
|
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Collect))
|
|
{
|
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
|
}
|
|
|
|
SliceResult result;
|
|
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.CommandStream))
|
|
{
|
|
result = _commandStream.ProcessOne();
|
|
}
|
|
|
|
switch (result)
|
|
{
|
|
case SliceResult.NoWork:
|
|
return;
|
|
case SliceResult.AllBlocked:
|
|
RetryBlockedCommandStreamIfDue();
|
|
return;
|
|
case SliceResult.BlockedWithoutProgress:
|
|
if (!_commandStream.HasUnblockedPending)
|
|
{
|
|
RetryBlockedCommandStreamIfDue();
|
|
return;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
_lastCommandStreamProgressTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
break;
|
|
}
|
|
|
|
slices++;
|
|
if (System.Diagnostics.Stopwatch.GetTimestamp() >= renderWorkDeadline)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RetryBlockedCommandStreamIfDue()
|
|
{
|
|
var now = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
if (now - _lastCommandStreamProgressTicks < _blockedRetryTicks)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_commandStream.RetryBlocked();
|
|
_lastCommandStreamProgressTicks = now;
|
|
}
|
|
|
|
// Milliseconds until the blocked heads are due for a retry; null when none is blocked.
|
|
private int? BlockedRetryWaitMilliseconds()
|
|
{
|
|
if (!_vulkanReady || !_commandStream.HasPending || _commandStream.HasUnblockedPending)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var remaining = _blockedRetryTicks - (System.Diagnostics.Stopwatch.GetTimestamp() - _lastCommandStreamProgressTicks);
|
|
return remaining <= 0 ? 0 : (int)Math.Max(1, remaining * 1000 / System.Diagnostics.Stopwatch.Frequency);
|
|
}
|
|
|
|
ICpuMemory ICommandStreamHost.Memory => _guestMemory;
|
|
|
|
public bool TryReadGuest(ulong address, Span<byte> destination)
|
|
{
|
|
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemorySync))
|
|
{
|
|
if (!_bufferCache.TrySynchronizeCpuRead(address, (ulong)destination.Length,
|
|
SharpEmu.HLE.GuestMemory.GuestMemoryProfile.ReadbackSource.CommandMemoryRead))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
using var readScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemoryRead);
|
|
return _guestMemory.TryRead(address, destination);
|
|
}
|
|
|
|
public void RunPendingCommands()
|
|
{
|
|
using var relayScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.QueueRelay);
|
|
_relay.RunPendingCommands();
|
|
}
|
|
|
|
public void BeginSubmission(int queueId, ulong submissionId, object? geometrySnapshots)
|
|
{
|
|
using var contextScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.QueueContext);
|
|
_activeGuestQueue = new VulkanGuestQueueIdentity(_commandQueueNames[queueId], submissionId);
|
|
BindSubmissionContext(_activeGuestQueue);
|
|
_ = CurrentRecordingBuffer();
|
|
_translation.BeginSubmission(queueId, submissionId, geometrySnapshots, _commandStream.GetInterpreter(queueId));
|
|
}
|
|
|
|
public void Flush()
|
|
{
|
|
using var flushScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.Flush);
|
|
FlushBatchedGuestCommands();
|
|
}
|
|
|
|
public void FlushAndWait()
|
|
{
|
|
using var waitScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandGpuWait);
|
|
_ = CurrentRecordingBuffer();
|
|
_scheduler.FlushAndWait();
|
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
|
}
|
|
|
|
public void SynchronizeGpu()
|
|
{
|
|
using var waitScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandGpuWait);
|
|
_ = CurrentRecordingBuffer();
|
|
_scheduler.Finish();
|
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
|
}
|
|
|
|
public void RunGarbageCollector() => RunGuestCacheCollection();
|
|
|
|
public void EmitGlobalBarrier()
|
|
{
|
|
EndRendering();
|
|
EndRendering();
|
|
var commandBuffer = BeginBatchedGuestCommands();
|
|
var barrier = new MemoryBarrier2
|
|
{
|
|
SType = StructureType.MemoryBarrier2,
|
|
SrcAccessMask = AccessFlags2.MemoryWriteBit,
|
|
DstAccessMask = AccessFlags2.MemoryReadBit | AccessFlags2.MemoryWriteBit,
|
|
};
|
|
VulkanSynchronization.PipelineBarrier(_vk,
|
|
commandBuffer,
|
|
PipelineStageFlags.AllCommandsBit,
|
|
PipelineStageFlags.AllCommandsBit,
|
|
0,
|
|
1,
|
|
&barrier,
|
|
0,
|
|
null,
|
|
0,
|
|
null);
|
|
}
|
|
|
|
public void FillBuffer(ulong address, ulong size, uint value, bool isGds)
|
|
{
|
|
using var transferScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemoryTransfer);
|
|
EndRendering();
|
|
_ = BeginBatchedGuestCommands();
|
|
_bufferCache.FillBuffer(address, size, value, isGds);
|
|
}
|
|
|
|
public void CopyBuffer(ulong destination, ulong source, ulong size, bool destinationIsGds, bool sourceIsGds)
|
|
{
|
|
using var transferScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemoryTransfer);
|
|
EndRendering();
|
|
_ = BeginBatchedGuestCommands();
|
|
_bufferCache.CopyBuffer(destination, source, size, destinationIsGds, sourceIsGds);
|
|
}
|
|
|
|
public void ReadGds(Span<uint> destination, uint wordOffset, uint wordCount) =>
|
|
EndOfPipe.ReadGdsWords(_bufferCache.GdsBuffer.Mapped, destination, wordOffset, wordCount);
|
|
|
|
public void RecordEndOfPipe(in EndOfPipeWrite write)
|
|
{
|
|
using var completionScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandEndOfPipe);
|
|
_ = BeginBatchedGuestCommands();
|
|
var buffer = _scheduler.Current;
|
|
switch (write.Kind)
|
|
{
|
|
case EndOfPipeWriteKind.Write32:
|
|
_endOfPipe.RecordWrite32(write.SubmitId, buffer, write.Destination, (uint)write.Value);
|
|
break;
|
|
case EndOfPipeWriteKind.Write64:
|
|
_endOfPipe.RecordWrite64(write.SubmitId, buffer, write.Destination, write.Value);
|
|
break;
|
|
case EndOfPipeWriteKind.WriteBack32:
|
|
_endOfPipe.RecordWrite32WithWriteBack(write.SubmitId, buffer, write.Destination, (uint)write.Value);
|
|
break;
|
|
case EndOfPipeWriteKind.WriteBack64:
|
|
_endOfPipe.RecordWrite64WithWriteBack(write.SubmitId, buffer, write.Destination, write.Value);
|
|
break;
|
|
case EndOfPipeWriteKind.InterruptOnly:
|
|
_endOfPipe.QueueInterruptOnCompletion(buffer, write.EventId, write.ContextId);
|
|
break;
|
|
case EndOfPipeWriteKind.Interrupt32:
|
|
_endOfPipe.RecordWrite32WithInterrupt(write.SubmitId, buffer, write.Destination, (uint)write.Value, write.EventId, write.ContextId);
|
|
break;
|
|
case EndOfPipeWriteKind.Interrupt64:
|
|
_endOfPipe.RecordWrite64WithInterrupt(write.SubmitId, buffer, write.Destination, write.Value, write.EventId, write.ContextId);
|
|
break;
|
|
case EndOfPipeWriteKind.InterruptWriteBack32:
|
|
_endOfPipe.RecordWrite32WithInterruptAndWriteBack(write.SubmitId, buffer, write.Destination, (uint)write.Value, write.EventId, write.ContextId);
|
|
break;
|
|
case EndOfPipeWriteKind.InterruptWriteBack64:
|
|
_endOfPipe.RecordWrite64WithInterruptAndWriteBack(write.SubmitId, buffer, write.Destination, write.Value, write.EventId, write.ContextId);
|
|
break;
|
|
case EndOfPipeWriteKind.GdsWrite32:
|
|
_endOfPipe.RecordGdsWrite32(write.SubmitId, buffer, write.Destination, write.GdsWordOffset, write.GdsWordCount);
|
|
break;
|
|
case EndOfPipeWriteKind.ClockWrite:
|
|
_endOfPipe.RecordClockWrite(write.SubmitId, buffer, write.Destination);
|
|
break;
|
|
case EndOfPipeWriteKind.ClockWriteBack:
|
|
_endOfPipe.RecordClockWriteWithWriteBack(write.SubmitId, buffer, write.Destination);
|
|
break;
|
|
case EndOfPipeWriteKind.Flip:
|
|
_endOfPipe.RecordFlipCompletion(write.SubmitId, buffer, write.FlipHandle, write.FlipIndex, write.FlipMode, write.FlipArgument, write.FlipRequestId);
|
|
break;
|
|
case EndOfPipeWriteKind.FlipWithWrite32:
|
|
_endOfPipe.RecordWrite32WithFlip(write.SubmitId, buffer, write.Destination, (uint)write.Value, write.FlipHandle, write.FlipIndex, write.FlipMode, write.FlipArgument, write.FlipRequestId);
|
|
break;
|
|
case EndOfPipeWriteKind.FlipWithInterruptWriteBack32:
|
|
_endOfPipe.RecordWrite32WithInterruptWriteBackAndFlip(write.SubmitId, buffer, write.Destination, (uint)write.Value, write.FlipHandle, write.FlipIndex, write.FlipMode, write.FlipArgument, write.FlipRequestId, write.EventId);
|
|
break;
|
|
default:
|
|
throw SubmissionScheduler.Fatal($"The end-of-pipe write kind is unknown: kind={write.Kind}.");
|
|
}
|
|
}
|
|
|
|
public void TriggerInterrupt(int eventId, uint contextId) => _endOfPipeEvents.TriggerInterrupt(eventId, contextId);
|
|
|
|
public bool HasFlipSlot()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _pendingGuestImagePresentations.Count < MaxPendingGuestFlipVersions;
|
|
}
|
|
}
|
|
|
|
public ulong PrepareFlip(int handle, int index, int flipMode, long flipArgument)
|
|
{
|
|
_ = BeginBatchedGuestCommands();
|
|
return _endOfPipe.PrepareVideoOutFlip(_scheduler.Current, handle, index, flipMode, flipArgument);
|
|
}
|
|
|
|
public bool IsFlipDone(int handle, int index) => VideoOutExports.IsFlipDone(handle, index);
|
|
|
|
public void PrepareCpuFlip(int handle, int index, ulong requestId) =>
|
|
CaptureFlip(handle, index, requestId, flipMode: 0, flipArg: 0);
|
|
|
|
public void DrawIndexed(ulong submitId, in DrawIndexedArguments arguments)
|
|
{
|
|
using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDrawTranslation);
|
|
var started = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
_translation.DrawIndexed(submitId, in arguments);
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Add(ref _perfDrawTicks, System.Diagnostics.Stopwatch.GetTimestamp() - started);
|
|
}
|
|
}
|
|
|
|
public void DrawAuto(ulong submitId, in DrawAutoArguments arguments)
|
|
{
|
|
using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDrawTranslation);
|
|
var started = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
_translation.DrawAuto(submitId, in arguments);
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Add(ref _perfDrawTicks, System.Diagnostics.Stopwatch.GetTimestamp() - started);
|
|
}
|
|
}
|
|
|
|
// RenderExecutor records indirect workgroup dispatches with vkCmdDispatchIndirect.
|
|
// SHARPEMU_CPU_INDIRECT_DISPATCH=1 reads the counts back on the CPU as before.
|
|
public bool ResolvesIndirectDispatchOnGpu => !_cpuIndirectDispatch;
|
|
|
|
// RenderExecutor records indexed indirect draws with vkCmdDrawIndexedIndirect.
|
|
// SHARPEMU_CPU_INDIRECT_DRAW=1 reads the arguments back on the CPU as before.
|
|
public bool ResolvesIndirectDrawOnGpu => !_cpuIndirectDraw;
|
|
|
|
private static readonly bool _cpuIndirectDraw = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DRAW"), "1", StringComparison.Ordinal);
|
|
|
|
private static readonly bool _cpuIndirectDispatch = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DISPATCH"), "1", StringComparison.Ordinal);
|
|
|
|
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
|
|
{
|
|
using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDispatchTranslation);
|
|
var started = System.Diagnostics.Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
_translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Add(ref _perfDrawTicks, System.Diagnostics.Stopwatch.GetTimestamp() - started);
|
|
}
|
|
}
|
|
|
|
public void OnQueueReset(int queueId) => _translation.QueueReset(queueId);
|
|
|
|
public Exception Fatal(string message) => SubmissionScheduler.Fatal(message);
|
|
|
|
// The flip packet: translation first, then the request, then the capture in stream order.
|
|
public int SubmitFlipFromGpu(RecordingBuffer buffer, int handle, int index, int flipMode, long flipArg, out ulong requestId)
|
|
{
|
|
_ = buffer;
|
|
_translation.PrepareFlip(handle, index);
|
|
var result = VideoOutExports.TryReserveFlipRequest(handle, index, flipMode, flipArg, gpuQueued: true, out requestId);
|
|
if (result != 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
CaptureFlip(handle, index, requestId, flipMode, flipArg);
|
|
}
|
|
catch
|
|
{
|
|
VideoOutExports.CancelFlip(requestId);
|
|
throw;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The interpreter must suspend before capture so this worker can present queued frames.
|
|
public void WaitForSubmitSlot() => throw SubmissionScheduler.Fatal("The GPU worker cannot wait for a flip slot. Suspend the command stream before capture.");
|
|
|
|
public void CompleteFlip(ulong requestId) => VideoOutExports.CompleteFlip(requestId);
|
|
|
|
// Copies the display surface through the store; presentation shows the copy once its tick retires.
|
|
internal void CaptureFlip(int handle, int index, ulong requestId, int flipMode, long flipArg)
|
|
{
|
|
// A no-buffer flip has no image to capture; its completion still retires in order.
|
|
if (VideoOutExports.ReleaseNoBufferFlip(index, requestId))
|
|
{
|
|
return;
|
|
}
|
|
if (!VideoOutExports.TryGetDisplayBufferInfo(handle, index, out var displayBuffer))
|
|
{
|
|
throw SubmissionScheduler.Fatal($"The flip names a display buffer that is not registered: handle={handle} index={index} request={requestId}.");
|
|
}
|
|
|
|
if (_deviceLost)
|
|
{
|
|
VideoOutExports.DiscardFlip(requestId);
|
|
return;
|
|
}
|
|
|
|
FlushBatchedGuestCommands();
|
|
RunGuestCacheCollection();
|
|
EnsureGuestSubmissionCapacity();
|
|
long version;
|
|
lock (_gate)
|
|
{
|
|
version = ++_guestFlipVersionSequence;
|
|
}
|
|
|
|
var submitted = false;
|
|
GuestImageResource? snapshot = null;
|
|
try
|
|
{
|
|
var surface = new DisplaySurfaceWords(
|
|
displayBuffer.Address, 0, displayBuffer.PixelFormat, displayBuffer.Width, displayBuffer.Height, displayBuffer.TilingMode, 0, 0, 0, false);
|
|
var request = ImageRequestBuilders.DisplaySurface(surface);
|
|
_ = BeginBatchedGuestCommands();
|
|
var imageIdentifier = _imageCache.FindImage(ref request);
|
|
var source = _imageCache.GetImage(imageIdentifier);
|
|
source.Uses.VideoOut = true;
|
|
_imageCache.RefreshImage(imageIdentifier);
|
|
// The refresh can end the tick; the copy records into the buffer that is current now.
|
|
var commandBuffer = BeginBatchedGuestCommands();
|
|
var extent = source.Backing.Extent;
|
|
snapshot = CreateGuestFlipSnapshot(GetPresentationSnapshotFormat(source.Backing.Format),
|
|
extent.Width, extent.Height, displayBuffer.Address, version);
|
|
source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, null, commandBuffer);
|
|
var toTransferDst = new ImageMemoryBarrier2
|
|
{
|
|
SType = StructureType.ImageMemoryBarrier2,
|
|
SrcAccessMask = 0,
|
|
DstAccessMask = AccessFlags2.TransferWriteBit,
|
|
OldLayout = ImageLayout.Undefined,
|
|
NewLayout = ImageLayout.TransferDstOptimal,
|
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
Image = snapshot.Image,
|
|
SubresourceRange = ColorSubresourceRange(),
|
|
};
|
|
VulkanSynchronization.PipelineBarrier(_vk,
|
|
commandBuffer, PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.TransferBit, 0, 0, null, 0, null, 1, &toTransferDst);
|
|
var copy = new ImageCopy
|
|
{
|
|
SrcSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
|
DstSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
|
Extent = new Extent3D(extent.Width, extent.Height, 1),
|
|
};
|
|
_vk.CmdCopyImage(
|
|
commandBuffer, source.Backing.Handle, ImageLayout.TransferSrcOptimal, snapshot.Image, ImageLayout.TransferDstOptimal, 1, ©);
|
|
var toShaderRead = new ImageMemoryBarrier2
|
|
{
|
|
SType = StructureType.ImageMemoryBarrier2,
|
|
SrcAccessMask = AccessFlags2.TransferWriteBit,
|
|
DstAccessMask = AccessFlags2.ShaderReadBit | AccessFlags2.TransferReadBit,
|
|
OldLayout = ImageLayout.TransferDstOptimal,
|
|
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
|
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
Image = snapshot.Image,
|
|
SubresourceRange = ColorSubresourceRange(),
|
|
};
|
|
VulkanSynchronization.PipelineBarrier(_vk,
|
|
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit, 0, 0, null, 0, null, 1, &toShaderRead);
|
|
|
|
FlushBatchedGuestCommands();
|
|
submitted = true;
|
|
_guestImageVersions.Add(version, snapshot);
|
|
|
|
lock (_gate)
|
|
{
|
|
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
|
|
var presentation = new Presentation(
|
|
null,
|
|
displayBuffer.Width,
|
|
displayBuffer.Height,
|
|
sequence,
|
|
GuestDrawKind.None,
|
|
IsSplash: false,
|
|
GuestImageAddress: displayBuffer.Address,
|
|
GuestImageVersion: version,
|
|
IsHdr: VideoOutExports.IsHdrPixelFormat(displayBuffer.PixelFormat),
|
|
RequiredTick: _submitTimeline,
|
|
FlipRequestId: requestId);
|
|
_latestPresentation = presentation;
|
|
_pendingGuestImagePresentations.Enqueue(presentation);
|
|
while (_pendingGuestImagePresentations.Count > MaxPendingGuestFlipVersions)
|
|
{
|
|
RetirePresentation(_pendingGuestImagePresentations.Dequeue());
|
|
}
|
|
}
|
|
|
|
CollectAbandonedGuestImageVersions();
|
|
TraceVulkanShader(
|
|
$"vk.flip_capture version={version} " +
|
|
$"queue={_activeGuestQueue.Name} submission={_activeGuestQueue.SubmissionId} " +
|
|
$"request={requestId} addr=0x{displayBuffer.Address:X16} " +
|
|
$"size={extent.Width}x{extent.Height}");
|
|
RenderDocCapture.OnGuestFlipBoundary(version);
|
|
VideoOutExports.TraceGpuFlip(_guestMemory, handle, index, flipMode, flipArg, displayBuffer.Address, VideoOutExports.GetFlipEventCount(requestId));
|
|
}
|
|
finally
|
|
{
|
|
if (!submitted)
|
|
{
|
|
VideoOutExports.DiscardFlip(requestId);
|
|
if (snapshot is not null)
|
|
{
|
|
DestroyGuestImage(snapshot);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The presenter drops a frame it will never show: its flip no longer blocks the stream.
|
|
private static void RetirePresentation(in Presentation presentation)
|
|
{
|
|
if (presentation.FlipRequestId != 0)
|
|
{
|
|
VideoOutExports.DiscardFlip(presentation.FlipRequestId);
|
|
}
|
|
}
|
|
|
|
// The presenter is done with the frame; only a successful present counts as presented.
|
|
private void CompletePresentation(in Presentation presentation, bool presented)
|
|
{
|
|
_presentedSequence = presentation.Sequence;
|
|
if (presentation.FlipRequestId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (presented)
|
|
{
|
|
VideoOutExports.MarkFlipPresented(presentation.FlipRequestId);
|
|
}
|
|
else
|
|
{
|
|
VideoOutExports.DiscardFlip(presentation.FlipRequestId);
|
|
}
|
|
|
|
_commandStream.RetryBlocked();
|
|
}
|
|
|
|
// Keep GPU completion separate from refresh eligibility.
|
|
private bool IsPresentationReadyLocked(in Presentation presentation) =>
|
|
(presentation.RequiredTick == 0 || (_scheduler is not null && _scheduler.Timeline.CompletedTick >= presentation.RequiredTick)) &&
|
|
VideoOutExports.CanPresentFlip(presentation.FlipRequestId, System.Diagnostics.Stopwatch.GetTimestamp());
|
|
|
|
private bool TryTakePresentation(out Presentation presentation)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
// Remove cancelled frames without releasing their in-flight image resources.
|
|
while (_pendingGuestImagePresentations.Count > 0 &&
|
|
(_pendingGuestImagePresentations.Peek().Sequence <= _presentedSequence ||
|
|
!VideoOutExports.IsFlipPresentationPending(_pendingGuestImagePresentations.Peek().FlipRequestId)))
|
|
{
|
|
var retired = _pendingGuestImagePresentations.Dequeue();
|
|
RetirePresentation(retired);
|
|
if (_latestPresentation is { } last && last.Sequence == retired.Sequence)
|
|
{
|
|
_latestPresentation = null;
|
|
}
|
|
_commandStream.RetryBlocked();
|
|
}
|
|
|
|
if (_pendingGuestImagePresentations.Count > 0)
|
|
{
|
|
var pending = _pendingGuestImagePresentations.Peek();
|
|
if (IsPresentationReadyLocked(in pending))
|
|
{
|
|
presentation = _pendingGuestImagePresentations.Dequeue();
|
|
TryReplaceWithHostMovieFrame(ref presentation);
|
|
return true;
|
|
}
|
|
|
|
presentation = default;
|
|
return false;
|
|
}
|
|
|
|
while (_pendingVideoPresentations.Count > 0 &&
|
|
_pendingVideoPresentations.Peek().Sequence <= _presentedSequence)
|
|
{
|
|
_pendingVideoPresentations.Dequeue();
|
|
}
|
|
|
|
if (_pendingVideoPresentations.Count > 0)
|
|
{
|
|
presentation = _pendingVideoPresentations.Dequeue();
|
|
return true;
|
|
}
|
|
|
|
if (_latestPresentation is not { } latest ||
|
|
latest.Sequence == _presentedSequence ||
|
|
!IsPresentationReadyLocked(in latest))
|
|
{
|
|
if (_latestPresentation is { } rejected &&
|
|
rejected.GuestImageAddress != 0 &&
|
|
rejected.Sequence != _presentedSequence &&
|
|
_tracedGuestImagePresentRejections.Add(rejected.Sequence))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] vk.guest_present_rejected addr=0x{rejected.GuestImageAddress:X16} " +
|
|
$"seq={rejected.Sequence} presentedSeq={_presentedSequence} " +
|
|
$"required_tick={rejected.RequiredTick} completed_tick={_scheduler.Timeline.CompletedTick}");
|
|
}
|
|
|
|
presentation = default;
|
|
return false;
|
|
}
|
|
|
|
presentation = latest;
|
|
TryReplaceWithHostMovieFrame(ref presentation);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// True when a guest frame can be shown now; the wait loop wakes for it.
|
|
private bool HasReadyPresentationLocked()
|
|
{
|
|
if (_pendingGuestImagePresentations.Count > 0)
|
|
{
|
|
var pending = _pendingGuestImagePresentations.Peek();
|
|
return pending.Sequence <= _presentedSequence ||
|
|
!VideoOutExports.IsFlipPresentationPending(pending.FlipRequestId) ||
|
|
IsPresentationReadyLocked(in pending);
|
|
}
|
|
|
|
return _pendingVideoPresentations.Count > 0 ||
|
|
(_latestPresentation is { } latest && latest.Sequence != _presentedSequence && IsPresentationReadyLocked(in latest));
|
|
}
|
|
}
|
|
}
|