sharpemu/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Render...

503 lines
20 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.Gpu;
using SharpEmu.ShaderCompiler;
using Silk.NET.Vulkan;
internal static unsafe partial class VulkanVideoPresenter
{
private sealed partial class Presenter
{
// This partial drives the Vulkan presenter render loop.
private void ProcessGuestCacheReadbacks()
{
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.BufferFaults))
{
_bufferCache.ProcessPendingFaultBuffer();
}
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.ImageReadback))
{
_imageCache.FlushScheduledReadbacks();
}
}
private void RunGuestCacheCollection(bool endsFrame)
{
ProcessGuestCacheReadbacks();
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.ImageCollect))
{
_imageCache.RunGarbageCollector(endsFrame);
}
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.BufferCollect))
{
_bufferCache.RunGarbageCollector();
}
}
private void WaitForRenderWork()
{
using var profileScope = RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Idle);
var gpuWorkInFlight = _pendingGuestSubmissions.Count > 0 ||
Array.Exists(_frameInFlight, static pending => pending);
var blockedRetryWait = BlockedRetryWaitMilliseconds();
lock (_gate)
{
if (_closed ||
Volatile.Read(ref _presenterCloseRequested) ||
_relay.HasPendingCommands ||
(_vulkanReady && _commandStream.HasUnblockedPending) ||
blockedRetryWait == 0 ||
HasReadyPresentationLocked())
{
return;
}
var waitMilliseconds = gpuWorkInFlight ? 1 : 8;
if (blockedRetryWait is { } retryWait)
{
waitMilliseconds = Math.Min(waitMilliseconds, retryWait);
}
var waitPhase = _commandStream.HasPending
? RenderPhaseProfile.Phase.IdleBlockedCommands
: _pendingGuestImagePresentations.Count > 0 || _pendingVideoPresentations.Count > 0 ||
(_latestPresentation is { } latest && latest.Sequence != _presentedSequence)
? RenderPhaseProfile.Phase.IdlePendingPresentation
: RenderPhaseProfile.Phase.IdleNoQueuedWork;
using var waitProfile = RenderPhaseProfile.MeasureDetail(waitPhase);
SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.WaitStarted,
detail: waitMilliseconds);
var signaled = System.Threading.Monitor.Wait(_gate, waitMilliseconds);
// The result describes the monitor wait, not the arrival of runnable guest work.
SubmissionFlowProfile.Record(signaled ? SubmissionFlowProfile.EventKind.WaitSignaled
: SubmissionFlowProfile.EventKind.WaitTimedOut, detail: (int)waitPhase);
}
}
private void Render(double _)
{
try
{
RenderCore();
}
catch (Exception exception)
{
RenderDocCapture.DiscardFrame();
// Device loss can strike between any two Vulkan calls in the frame;
// keep the window loop pumping instead of tearing the presenter down.
if (!TryMarkDeviceLost(exception))
{
throw;
}
}
}
private void RenderCore()
{
using var profileScope = RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Unattributed);
RenderDocCapture.DiscardTimedOutFrame();
if (Volatile.Read(ref _presenterCloseRequested))
{
RenderDocCapture.DiscardFrame();
Console.Error.WriteLine("[LOADER][WARN] Vulkan VideoOut closing on host shutdown request.");
_window.Close();
return;
}
if (!_vulkanReady)
{
return;
}
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.QueueRelay))
{
_relay.RunPendingCommands();
}
if (_deviceLost)
{
RenderDocCapture.DiscardFrame();
_commandStream.DiscardAll();
return;
}
// Reuse of a frame slot waits only on that slot's tick, keeping
// up to MaxFramesInFlight frames pipelined between CPU and GPU.
var frameSlot = _currentFrameSlot;
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.FrameSlotWait))
{
WaitFrameSlot(frameSlot);
}
if (!_deviceLost)
{
using var collectScope = RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Collect);
CollectCompletedGuestSubmissions(waitForOldest: false);
}
var renderWorkDeadline = _renderWorkBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
: long.MaxValue;
RunCommandStreamSlices(renderWorkDeadline);
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Flush))
{
FlushBatchedGuestCommands();
}
PerfOverlay.SetGuestCacheStatistics(
_bufferCache.TotalUsedMemory, _imageCache.TotalUsedMemory, _deviceInfo.LiveAllocations, _deviceInfo.PeakAllocations);
CollectAbandonedGuestImageVersions();
using var preparationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.PresentationPreparation);
Presentation presentation;
if (_window.IsMinimized)
{
return;
}
var framebufferSize = GetFramebufferSize();
var drawableSizeChanged =
(uint)Math.Max(framebufferSize.X, 1) != _extent.Width ||
(uint)Math.Max(framebufferSize.Y, 1) != _extent.Height;
var hdrStateChanged = _window.ConsumeHdrStateChange();
var guestHdrRequestChanged =
_videoOptions.HdrMode == HostHdrMode.Auto &&
_hdrRequestedForSwapchain !=
(_window.HdrState.Enabled && VideoOutExports.IsHdrOutputRequested);
if (_window.ConsumeSurfaceRestore() ||
drawableSizeChanged ||
_swapchainRecreateDeferred ||
hdrStateChanged && _videoOptions.HdrMode != HostHdrMode.Off ||
guestHdrRequestChanged)
{
RecreateSwapchainResources(
guestHdrRequestChanged
? "guest HDR output change"
: hdrStateChanged
? "SDL HDR state change"
: drawableSizeChanged
? "SDL drawable resize"
: "restored SDL window",
Result.SuboptimalKhr);
return;
}
bool tookPresentation;
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.TakePresentation))
{
tookPresentation = TryTakePresentation(out presentation);
}
if (!tookPresentation &&
TryTakeHostMovieOnlyPresentation(_presentedSequence, out presentation))
{
tookPresentation = true;
}
if (!tookPresentation)
{
// A render-loop tick with no newer flip is normal.
if (SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.IsActive ||
ShouldTracePresentedGuestImageContentsForDiagnostics())
{
var hasPendingPresentation =
HasPendingGuestPresentation(_presentedSequence);
SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TracePresentNotTaken(
_presentedSequence,
hasPendingPresentation);
SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TraceGpuWaitSnapshot();
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
hasPendingPresentation &&
_presentNotTakenLoggedSequence != _presentedSequence)
{
_presentNotTakenLoggedSequence = _presentedSequence;
Console.Error.WriteLine(
$"[LOADER][WARN] vk.present_not_taken seq={_presentedSequence} " +
"— presentation submitted but its required GPU tick is incomplete; nothing shown.");
}
}
return;
}
SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TracePresentTaken(
presentation.Sequence,
presentation.GuestImageAddress,
presentation.GuestImageVersion);
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.present_taken addr=0x{presentation.GuestImageAddress:X16} " +
$"version={presentation.GuestImageVersion} " +
$"drawKind={presentation.DrawKind} hasPixels={presentation.Pixels is not null}");
}
if (presentation.Pixels is null &&
presentation.DrawKind != GuestDrawKind.FullscreenBarycentric &&
presentation.GuestImageAddress == 0)
{
CompletePresentation(in presentation, presented: false);
return;
}
byte[]? pixels = null;
if (presentation.Pixels is { } sourcePixels)
{
pixels = presentation.Width == _extent.Width && presentation.Height == _extent.Height
? sourcePixels
: ScaleBgra(
sourcePixels,
presentation.Width,
presentation.Height,
_extent.Width,
_extent.Height);
if ((ulong)pixels.Length > _stagingSize)
{
CompletePresentation(in presentation, presented: false);
return;
}
}
GuestImageResource? presentedGuestImage = null;
var ownsPresentedGuestImageVersion = false;
if (presentation.GuestImageVersion != 0)
{
ownsPresentedGuestImageVersion = _guestImageVersions.Remove(
presentation.GuestImageVersion,
out presentedGuestImage);
}
if (presentation.GuestImageAddress != 0 && presentedGuestImage is null)
{
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][WARN] vk.present_dropped addr=0x{presentation.GuestImageAddress:X16} " +
$"version={presentation.GuestImageVersion} " +
"found=False " +
$"— no swapchain present this frame (black).");
}
CompletePresentation(in presentation, presented: false);
return;
}
if (ownsPresentedGuestImageVersion)
{
System.Diagnostics.Debug.Assert(
_frameGuestImageVersions[frameSlot] is null,
"A reusable frame slot cannot still own a flip version.");
_frameGuestImageVersions[frameSlot] = presentedGuestImage;
}
if (presentedGuestImage is not null)
{
_directPresentationCount++;
var traceAddressedPresentation =
ShouldTraceAddressedPresentedGuestImage(presentedGuestImage);
if (traceAddressedPresentation ||
ShouldTracePresentedGuestImageContentsForDiagnostics() &&
(_directPresentationCount is 1 or 30 or 120 ||
_directPresentationCount % 600 == 0))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " +
$"addr=0x{presentedGuestImage.Address:X16}");
}
}
uint imageIndex;
Result acquireResult;
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Acquire))
{
acquireResult = _swapchainApi.AcquireNextImage(
_device,
_swapchain,
SwapchainAcquireTimeoutNs,
_frameImageAvailable[frameSlot],
default,
&imageIndex);
}
if (acquireResult == Result.Timeout)
{
ReleaseUnsubmittedPresentationResources(
frameSlot,
ownsPresentedGuestImageVersion,
presentedGuestImage);
return;
}
if (acquireResult == Result.ErrorOutOfDateKhr)
{
RecreateSwapchainResources("vkAcquireNextImageKHR", acquireResult);
ReleaseUnsubmittedPresentationResources(
frameSlot,
ownsPresentedGuestImageVersion,
presentedGuestImage);
CompletePresentation(in presentation, presented: false);
return;
}
CheckSwapchainResult(acquireResult, "vkAcquireNextImageKHR");
var recreateAfterPresent = acquireResult == Result.SuboptimalKhr;
if (pixels is not null)
{
var mapped = (void*)_frameUploadMapped[frameSlot];
fixed (byte* source = pixels)
{
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
}
}
using var presentScope = RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Present);
FlushBatchedGuestCommands();
_commandBuffer = CurrentRecordingBuffer();
PipelineStageFlags waitStage;
if (pixels is not null)
{
RecordUpload(imageIndex, frameSlot);
waitStage = PipelineStageFlags.TransferBit;
}
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
{
var clearValue = default(ClearValue);
var renderPassInfo = new RenderPassBeginInfo
{
SType = StructureType.RenderPassBeginInfo,
RenderPass = _renderPass,
Framebuffer = _framebuffers[imageIndex],
RenderArea = new Rect2D(new Offset2D(0, 0), _extent),
ClearValueCount = 1,
PClearValues = &clearValue,
};
_vk.CmdBeginRenderPass(
_commandBuffer,
&renderPassInfo,
SubpassContents.Inline);
_vk.CmdBindPipeline(
_commandBuffer,
PipelineBindPoint.Graphics,
_barycentricPipeline);
_vk.CmdDraw(_commandBuffer, 3, 1, 0, 0);
_vk.CmdEndRenderPass(_commandBuffer);
waitStage = PipelineStageFlags.ColorAttachmentOutputBit;
}
else if (presentedGuestImage is not null)
{
RecordGuestImageBlit(imageIndex, presentedGuestImage);
waitStage = PipelineStageFlags.TransferBit;
}
else
{
throw new InvalidOperationException(
$"Unsupported translated guest draw: {presentation.DrawKind}.");
}
if (PerfOverlay.DrawOnScreen)
{
RecordOverlayBlit(imageIndex, frameSlot);
}
if (_hdrOutputActive)
{
RecordHdrPresentation(imageIndex, presentation.IsHdr);
waitStage = PipelineStageFlags.ColorAttachmentOutputBit;
}
var renderFinished = _renderFinishedPerImage[imageIndex];
_submitTimeline = SubmitPresentation(
_frameImageAvailable[frameSlot],
waitStage,
renderFinished);
_commandBuffer = default;
_frameTimelines[frameSlot] = _submitTimeline;
_frameInFlight[frameSlot] = true;
var swapchain = _swapchain;
var presentInfo = new PresentInfoKHR
{
SType = StructureType.PresentInfoKhr,
WaitSemaphoreCount = 1,
PWaitSemaphores = &renderFinished,
SwapchainCount = 1,
PSwapchains = &swapchain,
PImageIndices = &imageIndex,
};
Result presentResult;
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.QueuePresent))
{
lock (_queueGate)
{
presentResult = _swapchainApi.QueuePresent(_queue, &presentInfo);
}
}
if (presentResult == Result.ErrorOutOfDateKhr)
{
// The submitted frame still executes; RecreateSwapchainResources
// drains it (and every frame slot) before destroying anything.
RecreateSwapchainResources("vkQueuePresentKHR", presentResult);
CompletePresentation(in presentation, presented: false);
return;
}
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
VideoOutExports.ReportPresentedFrame();
PerfOverlay.RecordPresent();
RenderPhaseProfile.RecordFrame();
if (_swapchainReadbackPending)
{
// Diagnostics read back GPU memory and need this frame done.
WaitFrameSlot(frameSlot);
TraceSwapchainReadback();
}
CollectCompletedGuestSubmissions(waitForOldest: false);
_imageInitialized[imageIndex] = true;
_currentFrameSlot = (frameSlot + 1) % MaxFramesInFlight;
CompletePresentation(in presentation, presented: true);
if (presentation.IsSplash && !_splashPresented)
{
_splashPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented splash: " +
$"{presentation.Width}x{presentation.Height}");
}
else if (!presentation.IsSplash && !_firstFramePresented)
{
_firstFramePresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented first frame: " +
$"{presentation.Width}x{presentation.Height}");
}
if (pixels is null && !_firstGuestDrawPresented)
{
_firstGuestDrawPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented guest frame: " +
(presentedGuestImage is not null
? $"image=0x{presentedGuestImage.Address:X16} " +
$"{presentedGuestImage.Width}x{presentedGuestImage.Height}"
: $"{presentation.DrawKind}"));
}
if (recreateAfterPresent)
{
RecreateSwapchainResources("present suboptimal", Result.SuboptimalKhr);
}
}
}
}