// 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? 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 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 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)); } } }