mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 08:29:53 +08:00
Fix Vulkan presenter synchronization and frame handling issues (#747)
* [VideoOut/Vulkan] Fix boot deadlock, writeback stall, and presentation bugs - Presenter thread now starts on the compute dispatch path too, fixing a boot deadlock when a title's first GPU work is compute (Ghost of Yotei G-Buffer clear). - Guest render-target format swaps between sibling pixel formats now reinterpret in place instead of recreating blank, preserving GPU-written content. - Vectorized the guest-buffer writeback scan (equal-byte skip + coarse per-page pre-check), fixing multi-second stalls on fragmented buffers that starved JobWorker completion signals. - _presentedSequence now advances on every Render() early-return path, fixing an unthrottled busy loop on stale/dropped presentations. * Remove unnecessary Silk.NET.Windowing dependency
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
public static class FlipProgressTracker
|
||||
{
|
||||
private static long _lastFlipTimestamp;
|
||||
private static long _lastFlipVersion;
|
||||
private static int _hasFlipped;
|
||||
|
||||
public static void RecordFlip(long version)
|
||||
{
|
||||
Volatile.Write(ref _lastFlipVersion, version);
|
||||
Volatile.Write(ref _lastFlipTimestamp, Stopwatch.GetTimestamp());
|
||||
Volatile.Write(ref _hasFlipped, 1);
|
||||
}
|
||||
|
||||
public static bool HasFlipped => Volatile.Read(ref _hasFlipped) != 0;
|
||||
|
||||
public static long LastFlipVersion => Volatile.Read(ref _lastFlipVersion);
|
||||
|
||||
public static double? SecondsSinceLastFlip()
|
||||
{
|
||||
if (Volatile.Read(ref _hasFlipped) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elapsedTicks = Stopwatch.GetTimestamp() - Volatile.Read(ref _lastFlipTimestamp);
|
||||
return elapsedTicks / (double)Stopwatch.Frequency;
|
||||
}
|
||||
}
|
||||
@@ -418,6 +418,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
? renderBudgetMs
|
||||
: OperatingSystem.IsMacOS() ? 12L : 0L) *
|
||||
System.Diagnostics.Stopwatch.Frequency / 1000L;
|
||||
private static readonly int _guestWorkFollowupWaitMs =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_RENDER_FOLLOWUP_WAIT_MS"),
|
||||
out var followupWaitMs) && followupWaitMs >= 0
|
||||
? followupWaitMs
|
||||
: 2;
|
||||
private static readonly long _guestWorkFollowupBudgetTicks =
|
||||
(long.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_RENDER_FOLLOWUP_BUDGET_MS"),
|
||||
out var followupBudgetMs) && followupBudgetMs >= 0
|
||||
? followupBudgetMs
|
||||
: 24L) *
|
||||
System.Diagnostics.Stopwatch.Frequency / 1000L;
|
||||
// Max time the main-thread Render() will block waiting for a frame slot's
|
||||
// GPU fence before skipping the frame and returning to the event pump.
|
||||
// Prevents the window freezing behind a slow-compute GPU backlog.
|
||||
@@ -551,6 +564,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WORK_COMPLETION"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceOrderedActionLatency =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_ORDERED_ACTION_LATENCY"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceGlobalWritebackTiming =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GLOBAL_WRITEBACK_TIMING"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
|
||||
_tracedGuestImageSubmissions = [];
|
||||
private static Thread? _thread;
|
||||
@@ -1511,6 +1534,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_guestImageWorkSequences[texture.Address] = workSequence;
|
||||
}
|
||||
}
|
||||
|
||||
if (_thread is null)
|
||||
{
|
||||
StartPresenterLocked();
|
||||
}
|
||||
}
|
||||
|
||||
return workSequence;
|
||||
@@ -2895,6 +2923,25 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitForFollowupGuestWork(int timeoutMilliseconds)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_pendingGuestWorkCount > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_closed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Threading.Monitor.Wait(_gate, timeoutMilliseconds);
|
||||
return _pendingGuestWorkCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CompleteGuestWork(in PendingGuestWork pending)
|
||||
{
|
||||
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
|
||||
@@ -3082,6 +3129,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
target is not null &&
|
||||
(state.TestEnable || state.WriteEnable || state.ClearEnable);
|
||||
|
||||
internal static bool RequiresRealFormatConversion(Format from, Format to)
|
||||
{
|
||||
static bool Is10Bit(Format f) =>
|
||||
f is Format.A2R10G10B10UnormPack32 or Format.A2B10G10R10UnormPack32;
|
||||
return (from == Format.R8G8B8A8Unorm && Is10Bit(to)) ||
|
||||
(Is10Bit(from) && to == Format.R8G8B8A8Unorm);
|
||||
}
|
||||
|
||||
private readonly record struct Presentation(
|
||||
byte[]? Pixels,
|
||||
uint Width,
|
||||
@@ -3569,6 +3624,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public RenderPass RenderPass;
|
||||
public RenderPass InitialRenderPass;
|
||||
public Framebuffer Framebuffer;
|
||||
public Dictionary<Format, ReinterpretedGuestImageViews> ReinterpretCache { get; } = new();
|
||||
public Dictionary<GuestDepthKey, DepthFramebufferResource> DepthFramebuffers { get; } = new();
|
||||
public bool Initialized;
|
||||
public bool InitialUploadPending;
|
||||
@@ -3577,6 +3633,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public bool SupportsStorageUsage;
|
||||
}
|
||||
|
||||
private readonly record struct ReinterpretedGuestImageViews(
|
||||
ImageView View,
|
||||
ImageView[] MipViews,
|
||||
RenderPass RenderPass,
|
||||
RenderPass InitialRenderPass,
|
||||
Framebuffer Framebuffer);
|
||||
|
||||
private sealed record PendingGuestSubmission(
|
||||
Fence Fence,
|
||||
CommandBuffer CommandBuffer,
|
||||
@@ -6059,6 +6122,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
_deferredGuestImageVersionDestroys.Dequeue();
|
||||
DestroyGuestImage(imageEntry.Image);
|
||||
FlipProgressTracker.RecordFlip(imageEntry.Image.FlipVersion);
|
||||
TraceVulkanShader(
|
||||
$"vk.flip_retired version={imageEntry.Image.FlipVersion} " +
|
||||
$"timeline={imageEntry.RetireTimeline} reason=presentation-dropped");
|
||||
@@ -6084,6 +6148,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_frameGuestImageVersions[slot] = null;
|
||||
_capturedGuestFlipVersions.Remove(unsubmittedVersion.FlipVersion);
|
||||
DestroyGuestImage(unsubmittedVersion);
|
||||
FlipProgressTracker.RecordFlip(unsubmittedVersion.FlipVersion);
|
||||
TraceVulkanShader(
|
||||
$"vk.flip_retired version={unsubmittedVersion.FlipVersion} " +
|
||||
$"frame_slot={slot} reason=frame-not-submitted");
|
||||
@@ -6117,6 +6182,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_frameGuestImageVersions[slot] = null;
|
||||
_capturedGuestFlipVersions.Remove(guestImageVersion.FlipVersion);
|
||||
DestroyGuestImage(guestImageVersion);
|
||||
FlipProgressTracker.RecordFlip(guestImageVersion.FlipVersion);
|
||||
TraceVulkanShader(
|
||||
$"vk.flip_retired version={guestImageVersion.FlipVersion} " +
|
||||
$"frame_slot={slot} timeline={_frameTimelines[slot]}");
|
||||
@@ -10157,7 +10223,26 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
var source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
||||
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
|
||||
if (!source.SequenceEqual(shadow))
|
||||
var needsRefresh = !source.SequenceEqual(shadow);
|
||||
if (!needsRefresh && _guestMemory is not null)
|
||||
{
|
||||
var live = GuestDataPool.Shared.Rent(guestBuffer.Length);
|
||||
try
|
||||
{
|
||||
var liveSpan = live.AsSpan(0, guestBuffer.Length);
|
||||
if (_guestMemory.TryRead(guestBuffer.BaseAddress, liveSpan) &&
|
||||
!liveSpan.SequenceEqual(shadow))
|
||||
{
|
||||
needsRefresh = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestDataPool.Shared.Return(live);
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRefresh)
|
||||
{
|
||||
if (!guestBuffer.Writable &&
|
||||
(allocation.LastUseTimeline > _completedTimeline ||
|
||||
@@ -11985,6 +12070,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var unreadablePages = 0;
|
||||
var fallbackWrites = 0;
|
||||
var firstChangedOffset = -1;
|
||||
var scanTicks = 0L;
|
||||
var ioTicks = 0L;
|
||||
allocation.DirtyRanges.RemoveAt(index);
|
||||
|
||||
// A writable descriptor only identifies a potential write
|
||||
@@ -11997,9 +12084,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// into millions of writes for alternating output patterns.
|
||||
const int pageSize = 4096;
|
||||
const int unreadableMergeGap = 16;
|
||||
const int FragmentationRunThreshold = 64;
|
||||
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||
var pageRuns = new List<(int Start, int Length)>(64);
|
||||
var coalescedPageRun = new List<(int Start, int Length)>(1);
|
||||
try
|
||||
{
|
||||
for (var pageStart = 0;
|
||||
@@ -12022,66 +12111,126 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
|
||||
mappedPageSource.CopyTo(mappedPage);
|
||||
pageRuns.Clear();
|
||||
var cursor = 0;
|
||||
while (cursor < pageLength)
|
||||
var scanStartTicks = _traceGlobalWritebackTiming
|
||||
? System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
: 0L;
|
||||
|
||||
const int coarseBlockSize = 128;
|
||||
var coarseBlockCount = 0;
|
||||
var coarseDiffBlockCount = 0;
|
||||
for (var blockStart = 0; blockStart < pageLength; blockStart += coarseBlockSize)
|
||||
{
|
||||
while (cursor < pageLength &&
|
||||
mappedPage[cursor] == shadowPage[cursor])
|
||||
var blockEnd = Math.Min(blockStart + coarseBlockSize, pageLength);
|
||||
coarseBlockCount++;
|
||||
if (!mappedPage.Slice(blockStart, blockEnd - blockStart).SequenceEqual(
|
||||
shadowPage.Slice(blockStart, blockEnd - blockStart)))
|
||||
{
|
||||
cursor++;
|
||||
coarseDiffBlockCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor == pageLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var runStart = cursor;
|
||||
while (cursor < pageLength &&
|
||||
mappedPage[cursor] != shadowPage[cursor])
|
||||
{
|
||||
cursor++;
|
||||
}
|
||||
|
||||
var runLength = cursor - runStart;
|
||||
pageRuns.Add((pageStart + runStart, runLength));
|
||||
if (coarseBlockCount > 0 && coarseDiffBlockCount * 4 >= coarseBlockCount)
|
||||
{
|
||||
pageRuns.Add((pageStart, pageLength));
|
||||
changedRuns++;
|
||||
changedBytes += (ulong)runLength;
|
||||
changedBytes += (ulong)pageLength;
|
||||
if (firstChangedOffset < 0)
|
||||
{
|
||||
firstChangedOffset = pageStart + runStart;
|
||||
firstChangedOffset = pageStart;
|
||||
}
|
||||
}
|
||||
else if (coarseDiffBlockCount > 0)
|
||||
{
|
||||
var cursor = 0;
|
||||
while (cursor < pageLength)
|
||||
{
|
||||
cursor = SkipEqualBytes(mappedPage, shadowPage, cursor, pageLength);
|
||||
|
||||
if (cursor == pageLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var runStart = cursor;
|
||||
cursor = SkipDifferentBytes(mappedPage, shadowPage, cursor, pageLength);
|
||||
|
||||
var runLength = cursor - runStart;
|
||||
pageRuns.Add((pageStart + runStart, runLength));
|
||||
changedRuns++;
|
||||
changedBytes += (ulong)runLength;
|
||||
if (firstChangedOffset < 0)
|
||||
{
|
||||
firstChangedOffset = pageStart + runStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_traceGlobalWritebackTiming)
|
||||
{
|
||||
scanTicks += System.Diagnostics.Stopwatch.GetTimestamp() - scanStartTicks;
|
||||
}
|
||||
|
||||
if (pageRuns.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<(int Start, int Length)> runsToWrite;
|
||||
if (pageRuns.Count > FragmentationRunThreshold)
|
||||
{
|
||||
coalescedPageRun.Clear();
|
||||
coalescedPageRun.Add((
|
||||
pageRuns[0].Start,
|
||||
pageRuns[^1].Start + pageRuns[^1].Length - pageRuns[0].Start));
|
||||
runsToWrite = coalescedPageRun;
|
||||
}
|
||||
else
|
||||
{
|
||||
runsToWrite = pageRuns;
|
||||
}
|
||||
|
||||
changedPages++;
|
||||
var livePage = livePageBuffer.AsSpan(0, pageLength);
|
||||
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
|
||||
var ioStartTicks = _traceGlobalWritebackTiming
|
||||
? System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
: 0L;
|
||||
var readOk = memory.TryRead(guestAddress + (ulong)pageStart, livePage);
|
||||
if (_traceGlobalWritebackTiming)
|
||||
{
|
||||
foreach (var run in pageRuns)
|
||||
ioTicks += System.Diagnostics.Stopwatch.GetTimestamp() - ioStartTicks;
|
||||
}
|
||||
|
||||
if (readOk)
|
||||
{
|
||||
foreach (var run in runsToWrite)
|
||||
{
|
||||
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||
livePage.Slice(run.Start - pageStart, run.Length));
|
||||
}
|
||||
|
||||
if (memory.TryWrite(guestAddress + (ulong)pageStart, livePage))
|
||||
var writeStartTicks = _traceGlobalWritebackTiming
|
||||
? System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
: 0L;
|
||||
var writeOk = memory.TryWrite(guestAddress + (ulong)pageStart, livePage);
|
||||
if (_traceGlobalWritebackTiming)
|
||||
{
|
||||
foreach (var run in pageRuns)
|
||||
ioTicks += System.Diagnostics.Stopwatch.GetTimestamp() - writeStartTicks;
|
||||
}
|
||||
|
||||
if (writeOk)
|
||||
{
|
||||
foreach (var run in runsToWrite)
|
||||
{
|
||||
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||
shadowBytes.Slice(run.Start, run.Length));
|
||||
}
|
||||
|
||||
writtenPages++;
|
||||
writtenRuns += pageRuns.Count;
|
||||
writtenRuns += runsToWrite.Count;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var run in pageRuns)
|
||||
foreach (var run in runsToWrite)
|
||||
{
|
||||
failedRuns++;
|
||||
MarkGuestBufferDirty(
|
||||
@@ -12221,10 +12370,63 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"probe_nonzero={nonzero}/{probe.Length} " +
|
||||
$"changed_head={Convert.ToHexString(head)}");
|
||||
}
|
||||
|
||||
if (_traceGlobalWritebackTiming && changedRuns > 0)
|
||||
{
|
||||
var freq = (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] vk.global_writeback_timing base=0x{guestAddress:X16} " +
|
||||
$"changed_runs={changedRuns} changed_pages={changedPages} " +
|
||||
$"scan_ms={(scanTicks * 1000.0 / freq).ToString("F1", System.Globalization.CultureInfo.InvariantCulture)} " +
|
||||
$"io_ms={(ioTicks * 1000.0 / freq).ToString("F1", System.Globalization.CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int SkipEqualBytes(
|
||||
ReadOnlySpan<byte> a,
|
||||
ReadOnlySpan<byte> b,
|
||||
int start,
|
||||
int end)
|
||||
{
|
||||
var cursor = start;
|
||||
var vectorSize = System.Numerics.Vector<byte>.Count;
|
||||
while (cursor + vectorSize <= end)
|
||||
{
|
||||
var va = new System.Numerics.Vector<byte>(a.Slice(cursor, vectorSize));
|
||||
var vb = new System.Numerics.Vector<byte>(b.Slice(cursor, vectorSize));
|
||||
if (va != vb)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
cursor += vectorSize;
|
||||
}
|
||||
|
||||
while (cursor < end && a[cursor] == b[cursor])
|
||||
{
|
||||
cursor++;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private static int SkipDifferentBytes(
|
||||
ReadOnlySpan<byte> a,
|
||||
ReadOnlySpan<byte> b,
|
||||
int start,
|
||||
int end)
|
||||
{
|
||||
var cursor = start;
|
||||
while (cursor < end && a[cursor] != b[cursor])
|
||||
{
|
||||
cursor++;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private void RecordChunkedComputeDispatch(
|
||||
CommandBuffer commandBuffer,
|
||||
VulkanComputeGuestDispatch work,
|
||||
@@ -13540,6 +13742,51 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (existing.Width == target.Width &&
|
||||
existing.Height == target.Height &&
|
||||
existing.MipLevels == mipLevels &&
|
||||
IsCompatibleViewFormat(existing.Format, format))
|
||||
{
|
||||
if (requiresStorage && !existing.SupportsStorageUsage)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Guest image 0x{target.Address:X16} was created without storage usage.");
|
||||
}
|
||||
|
||||
if (_traceGuestImageEvents)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GIMG] reinterpret addr=0x{target.Address:X} " +
|
||||
$"{existing.Format}->{format} {target.Width}x{target.Height} " +
|
||||
$"initialized={existing.Initialized}");
|
||||
}
|
||||
|
||||
ReinterpretGuestImageFormat(existing, format, !requiresStorage, target);
|
||||
existing.GuestFormat = guestFormat;
|
||||
existing.IsCpuBacked = false;
|
||||
existing.CpuContentFingerprint = 0;
|
||||
if (!requiresStorage && existing.RenderPass.Handle == 0)
|
||||
{
|
||||
var attachmentView = existing.MipViews.Length > 0
|
||||
? existing.MipViews[0]
|
||||
: existing.View;
|
||||
var promoted = CreateRenderPassAndFramebuffer(
|
||||
existing.Format,
|
||||
attachmentView,
|
||||
existing.Width,
|
||||
existing.Height);
|
||||
existing.RenderPass = promoted.RenderPass;
|
||||
existing.InitialRenderPass = promoted.InitialRenderPass;
|
||||
existing.Framebuffer = promoted.Framebuffer;
|
||||
var promotedName = GuestImageDebugName(target, format);
|
||||
SetDebugName(ObjectType.RenderPass, promoted.RenderPass.Handle, $"{promotedName} renderpass");
|
||||
SetDebugName(ObjectType.RenderPass, promoted.InitialRenderPass.Handle, $"{promotedName} initial-renderpass");
|
||||
SetDebugName(ObjectType.Framebuffer, promoted.Framebuffer.Handle, $"{promotedName} framebuffer");
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (_traceGuestImageEvents)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -14292,6 +14539,358 @@ internal static unsafe class VulkanVideoPresenter
|
||||
mipLevel >= 32
|
||||
? 1
|
||||
: Math.Max(dimension >> (int)mipLevel, 1u);
|
||||
private unsafe (Image Image, DeviceMemory Memory) CreateTransferScratchImage(
|
||||
Format format,
|
||||
uint width,
|
||||
uint height)
|
||||
{
|
||||
var imageInfo = new ImageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageCreateInfo,
|
||||
ImageType = ImageType.Type2D,
|
||||
Format = format,
|
||||
Extent = new Extent3D(width, height, 1),
|
||||
MipLevels = 1,
|
||||
ArrayLayers = 1,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
Tiling = ImageTiling.Optimal,
|
||||
Usage = ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
InitialLayout = ImageLayout.Undefined,
|
||||
};
|
||||
Check(_vk.CreateImage(_device, &imageInfo, null, out var image), "vkCreateImage(format-convert scratch)");
|
||||
_vk.GetImageMemoryRequirements(_device, image, out var requirements);
|
||||
var allocationInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.DeviceLocalBit),
|
||||
};
|
||||
Check(
|
||||
_vk.AllocateMemory(_device, &allocationInfo, null, out var memory),
|
||||
"vkAllocateMemory(format-convert scratch)");
|
||||
Check(_vk.BindImageMemory(_device, image, memory, 0), "vkBindImageMemory(format-convert scratch)");
|
||||
return (image, memory);
|
||||
}
|
||||
|
||||
private unsafe void ConvertGuestImageBytesInPlace(
|
||||
GuestImageResource resource,
|
||||
Format fromFormat,
|
||||
Format toFormat)
|
||||
{
|
||||
var (oldTyped, oldMemory) = CreateTransferScratchImage(fromFormat, resource.Width, resource.Height);
|
||||
var (newTyped, newMemory) = CreateTransferScratchImage(toFormat, resource.Width, resource.Height);
|
||||
try
|
||||
{
|
||||
var commandBuffer = AllocateGuestCommandBuffer();
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(
|
||||
_vk.BeginCommandBuffer(commandBuffer, &beginInfo),
|
||||
"vkBeginCommandBuffer(format-convert)");
|
||||
|
||||
var toTransferSrc = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.MemoryWriteBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
OldLayout = ImageLayout.General,
|
||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = resource.Image,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &toTransferSrc);
|
||||
|
||||
var oldTypedToDst = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = 0,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = oldTyped,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &oldTypedToDst);
|
||||
|
||||
var copyRegion = new ImageCopy
|
||||
{
|
||||
SrcSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
||||
SrcOffset = new Offset3D(0, 0, 0),
|
||||
DstSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
||||
DstOffset = new Offset3D(0, 0, 0),
|
||||
Extent = new Extent3D(resource.Width, resource.Height, 1),
|
||||
};
|
||||
_vk.CmdCopyImage(
|
||||
commandBuffer,
|
||||
resource.Image, ImageLayout.TransferSrcOptimal,
|
||||
oldTyped, ImageLayout.TransferDstOptimal,
|
||||
1, ©Region);
|
||||
|
||||
var oldTypedToSrc = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
OldLayout = ImageLayout.TransferDstOptimal,
|
||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = oldTyped,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &oldTypedToSrc);
|
||||
|
||||
var newTypedToDst = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = 0,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = newTyped,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &newTypedToDst);
|
||||
|
||||
var blitRegion = new ImageBlit
|
||||
{
|
||||
SrcSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
||||
SrcOffsets = new ImageBlit.SrcOffsetsBuffer
|
||||
{
|
||||
Element0 = new Offset3D(0, 0, 0),
|
||||
Element1 = new Offset3D(checked((int)resource.Width), checked((int)resource.Height), 1),
|
||||
},
|
||||
DstSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1),
|
||||
DstOffsets = new ImageBlit.DstOffsetsBuffer
|
||||
{
|
||||
Element0 = new Offset3D(0, 0, 0),
|
||||
Element1 = new Offset3D(checked((int)resource.Width), checked((int)resource.Height), 1),
|
||||
},
|
||||
};
|
||||
_vk.CmdBlitImage(
|
||||
commandBuffer,
|
||||
oldTyped, ImageLayout.TransferSrcOptimal,
|
||||
newTyped, ImageLayout.TransferDstOptimal,
|
||||
1, &blitRegion, Filter.Nearest);
|
||||
|
||||
var newTypedToSrc = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
OldLayout = ImageLayout.TransferDstOptimal,
|
||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = newTyped,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &newTypedToSrc);
|
||||
|
||||
var resourceToDst = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.TransferReadBit,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = ImageLayout.TransferSrcOptimal,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = resource.Image,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit,
|
||||
0, 0, null, 0, null, 1, &resourceToDst);
|
||||
|
||||
_vk.CmdCopyImage(
|
||||
commandBuffer,
|
||||
newTyped, ImageLayout.TransferSrcOptimal,
|
||||
resource.Image, ImageLayout.TransferDstOptimal,
|
||||
1, ©Region);
|
||||
|
||||
var resourceToGeneral = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit,
|
||||
OldLayout = ImageLayout.TransferDstOptimal,
|
||||
NewLayout = ImageLayout.General,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = resource.Image,
|
||||
SubresourceRange = ColorSubresourceRange(0, 1),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer, PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit,
|
||||
0, 0, null, 0, null, 1, &resourceToGeneral);
|
||||
|
||||
Check(_vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(format-convert)");
|
||||
SubmitGuestCommandBuffer(commandBuffer, [], []);
|
||||
|
||||
if (_traceGuestImageEvents)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[FORMAT-CONVERT] " +
|
||||
$"source_format={fromFormat} target_format={toFormat} " +
|
||||
$"address=0x{resource.Address:X16} " +
|
||||
"reason=bit-incompatible-view-reinterpret " +
|
||||
$"size={resource.Width}x{resource.Height}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_vk.DestroyImage(_device, oldTyped, null);
|
||||
_vk.FreeMemory(_device, oldMemory, null);
|
||||
_vk.DestroyImage(_device, newTyped, null);
|
||||
_vk.FreeMemory(_device, newMemory, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly bool _realFormatConversionEnabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_ENABLE_REAL_FORMAT_CONVERSION"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private void ReinterpretGuestImageFormat(
|
||||
GuestImageResource resource,
|
||||
Format format,
|
||||
bool promoteRenderPass,
|
||||
GuestRenderTarget target)
|
||||
{
|
||||
if (_realFormatConversionEnabled &&
|
||||
RequiresRealFormatConversion(resource.Format, format))
|
||||
{
|
||||
ConvertGuestImageBytesInPlace(resource, resource.Format, format);
|
||||
foreach (var cachedEntry in resource.ReinterpretCache.Values)
|
||||
{
|
||||
DestroyReinterpretedGuestImageViews(cachedEntry);
|
||||
}
|
||||
resource.ReinterpretCache.Clear();
|
||||
}
|
||||
|
||||
var previous = new ReinterpretedGuestImageViews(
|
||||
resource.View,
|
||||
resource.MipViews,
|
||||
resource.RenderPass,
|
||||
resource.InitialRenderPass,
|
||||
resource.Framebuffer);
|
||||
resource.ReinterpretCache.TryAdd(resource.Format, previous);
|
||||
|
||||
if (resource.ReinterpretCache.Remove(format, out var cached))
|
||||
{
|
||||
resource.View = cached.View;
|
||||
resource.MipViews = cached.MipViews;
|
||||
resource.RenderPass = cached.RenderPass;
|
||||
resource.InitialRenderPass = cached.InitialRenderPass;
|
||||
resource.Framebuffer = cached.Framebuffer;
|
||||
resource.Format = format;
|
||||
return;
|
||||
}
|
||||
|
||||
var viewInfo = new ImageViewCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageViewCreateInfo,
|
||||
Image = resource.Image,
|
||||
ViewType = ImageViewType.Type2D,
|
||||
Format = format,
|
||||
Components = new ComponentMapping(
|
||||
ComponentSwizzle.Identity,
|
||||
ComponentSwizzle.Identity,
|
||||
ComponentSwizzle.Identity,
|
||||
ComponentSwizzle.Identity),
|
||||
SubresourceRange = ColorSubresourceRange(0, resource.MipLevels),
|
||||
};
|
||||
Check(
|
||||
_vk.CreateImageView(_device, &viewInfo, null, out var newView),
|
||||
"vkCreateImageView(guest reinterpret)");
|
||||
resource.View = newView;
|
||||
|
||||
var mipViews = new ImageView[resource.MipLevels];
|
||||
for (uint mipLevel = 0; mipLevel < resource.MipLevels; mipLevel++)
|
||||
{
|
||||
viewInfo.SubresourceRange = ColorSubresourceRange(mipLevel, 1);
|
||||
Check(
|
||||
_vk.CreateImageView(_device, &viewInfo, null, out var mipView),
|
||||
"vkCreateImageView(guest reinterpret mip)");
|
||||
mipViews[mipLevel] = mipView;
|
||||
}
|
||||
|
||||
resource.MipViews = mipViews;
|
||||
resource.Format = format;
|
||||
|
||||
if (promoteRenderPass)
|
||||
{
|
||||
var attachmentView = resource.MipViews.Length > 0
|
||||
? resource.MipViews[0]
|
||||
: resource.View;
|
||||
var promoted = CreateRenderPassAndFramebuffer(
|
||||
resource.Format,
|
||||
attachmentView,
|
||||
resource.Width,
|
||||
resource.Height);
|
||||
resource.RenderPass = promoted.RenderPass;
|
||||
resource.InitialRenderPass = promoted.InitialRenderPass;
|
||||
resource.Framebuffer = promoted.Framebuffer;
|
||||
var promotedName = GuestImageDebugName(target, format);
|
||||
SetDebugName(ObjectType.RenderPass, promoted.RenderPass.Handle, $"{promotedName} renderpass");
|
||||
SetDebugName(ObjectType.RenderPass, promoted.InitialRenderPass.Handle, $"{promotedName} initial-renderpass");
|
||||
SetDebugName(ObjectType.Framebuffer, promoted.Framebuffer.Handle, $"{promotedName} framebuffer");
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyReinterpretedGuestImageViews(ReinterpretedGuestImageViews views)
|
||||
{
|
||||
if (views.Framebuffer.Handle != 0)
|
||||
{
|
||||
_vk.DestroyFramebuffer(_device, views.Framebuffer, null);
|
||||
}
|
||||
|
||||
if (views.RenderPass.Handle != 0)
|
||||
{
|
||||
_vk.DestroyRenderPass(_device, views.RenderPass, null);
|
||||
}
|
||||
|
||||
if (views.InitialRenderPass.Handle != 0)
|
||||
{
|
||||
_vk.DestroyRenderPass(_device, views.InitialRenderPass, null);
|
||||
}
|
||||
|
||||
if (views.View.Handle != 0)
|
||||
{
|
||||
_vk.DestroyImageView(_device, views.View, null);
|
||||
}
|
||||
|
||||
foreach (var mipView in views.MipViews)
|
||||
{
|
||||
if (mipView.Handle != 0)
|
||||
{
|
||||
_vk.DestroyImageView(_device, mipView, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyGuestImage(GuestImageResource resource)
|
||||
{
|
||||
@@ -14310,6 +14909,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
resource.FormatViews.Clear();
|
||||
|
||||
foreach (var cached in resource.ReinterpretCache.Values)
|
||||
{
|
||||
DestroyReinterpretedGuestImageViews(cached);
|
||||
}
|
||||
resource.ReinterpretCache.Clear();
|
||||
|
||||
if (resource.Framebuffer.Handle != 0)
|
||||
{
|
||||
_vk.DestroyFramebuffer(_device, resource.Framebuffer, null);
|
||||
@@ -14517,9 +15122,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
format switch
|
||||
{
|
||||
Format.R8Unorm or
|
||||
Format.R8SNorm or
|
||||
Format.R8Srgb or
|
||||
Format.R8Uint or
|
||||
Format.R8Sint or
|
||||
Format.R8SNorm => 8,
|
||||
Format.R8Sint => 8,
|
||||
// Every single-channel 16-bit format shares this class, not just
|
||||
// the float one. Omitting the rest made GetVulkanImageByteCount
|
||||
// return zero for them, and a zero expected size rejects the
|
||||
@@ -14533,26 +15139,36 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Format.R16Sint or
|
||||
Format.R8G8Unorm or
|
||||
Format.R8G8SNorm or
|
||||
Format.R8G8Srgb or
|
||||
Format.R8G8Uint or
|
||||
Format.R8G8Sint => 16,
|
||||
Format.R32Uint or
|
||||
Format.R32Sint or
|
||||
Format.R32Sfloat or
|
||||
Format.R16G16Unorm or
|
||||
Format.R16G16SNorm or
|
||||
Format.R16G16Uint or
|
||||
Format.R16G16Sint or
|
||||
Format.R16G16Sfloat or
|
||||
Format.R8G8B8A8Unorm or
|
||||
Format.R8G8B8A8SNorm or
|
||||
Format.R8G8B8A8Srgb or
|
||||
Format.R8G8B8A8Uint or
|
||||
Format.R8G8B8A8Sint or
|
||||
Format.B8G8R8A8Unorm or
|
||||
Format.B8G8R8A8SNorm or
|
||||
Format.B8G8R8A8Srgb or
|
||||
Format.A8B8G8R8UnormPack32 or
|
||||
Format.A8B8G8R8SrgbPack32 or
|
||||
Format.A2R10G10B10UnormPack32 or
|
||||
Format.A2B10G10R10UnormPack32 or
|
||||
Format.B10G11R11UfloatPack32 => 32,
|
||||
Format.B10G11R11UfloatPack32 or
|
||||
Format.E5B9G9R9UfloatPack32 => 32,
|
||||
Format.R32G32Uint or
|
||||
Format.R32G32Sint or
|
||||
Format.R32G32Sfloat or
|
||||
Format.R16G16B16A16Unorm or
|
||||
Format.R16G16B16A16SNorm or
|
||||
Format.R16G16B16A16Uint or
|
||||
Format.R16G16B16A16Sint or
|
||||
Format.R16G16B16A16Sfloat => 64,
|
||||
@@ -14660,10 +15276,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
var completedWork = 0;
|
||||
HashSet<string>? deferredOrderedQueues = null;
|
||||
var workBudgetTicks = _renderWorkBudgetTicks;
|
||||
var renderWorkDeadline = workBudgetTicks > 0
|
||||
? System.Diagnostics.Stopwatch.GetTimestamp() + workBudgetTicks
|
||||
var drainStartTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
var renderWorkDeadline = _renderWorkBudgetTicks > 0
|
||||
? drainStartTicks + _renderWorkBudgetTicks
|
||||
: long.MaxValue;
|
||||
var followupDeadline = _guestWorkFollowupBudgetTicks > 0
|
||||
? drainStartTicks + _guestWorkFollowupBudgetTicks
|
||||
: long.MinValue;
|
||||
var workLimit = _maxGuestWorkPerRender;
|
||||
// Prefer ordered sync / flip heads while the queue is elevated so
|
||||
// label wakeups are not starved behind fat compute/draw items on
|
||||
@@ -14701,7 +15320,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
if (!tookGuestWork)
|
||||
{
|
||||
break;
|
||||
var nowTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (completedWork == 0 ||
|
||||
_guestWorkFollowupWaitMs <= 0 ||
|
||||
nowTicks >= followupDeadline ||
|
||||
nowTicks >= renderWorkDeadline ||
|
||||
!WaitForFollowupGuestWork(_guestWorkFollowupWaitMs))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(
|
||||
@@ -14746,6 +15375,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"queued_ms={(System.Diagnostics.Stopwatch.GetTimestamp() - pendingGuestWork.EnqueuedTicks) * 1000.0 / System.Diagnostics.Stopwatch.Frequency:F3} " +
|
||||
work.GetType().Name);
|
||||
}
|
||||
|
||||
if (_traceOrderedActionLatency && work is VulkanOrderedGuestAction orderedActionForLatency)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] vk.ordered_action_latency #{completedWork} " +
|
||||
$"name='{orderedActionForLatency.DebugName}' " +
|
||||
$"queue={pendingGuestWork.Queue.Name} " +
|
||||
$"queued_ms={(System.Diagnostics.Stopwatch.GetTimestamp() - pendingGuestWork.EnqueuedTicks) * 1000.0 / System.Diagnostics.Stopwatch.Frequency:F3} " +
|
||||
$"pending={_pendingGuestWorkCount}");
|
||||
}
|
||||
try
|
||||
{
|
||||
|
||||
@@ -14963,6 +15602,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
presentation.TranslatedDraw is null &&
|
||||
presentation.GuestImageAddress == 0)
|
||||
{
|
||||
_presentedSequence = presentation.Sequence;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14979,6 +15619,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_extent.Height);
|
||||
if ((ulong)pixels.Length > _stagingSize)
|
||||
{
|
||||
_presentedSequence = presentation.Sequence;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15018,6 +15659,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
DestroyGuestImage(presentedGuestImage);
|
||||
}
|
||||
|
||||
_presentedSequence = presentation.Sequence;
|
||||
return;
|
||||
}
|
||||
if (ownsPresentedGuestImageVersion)
|
||||
@@ -15105,6 +15747,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
ownsPresentedGuestImageVersion,
|
||||
presentedGuestImage);
|
||||
|
||||
_presentedSequence = presentation.Sequence;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15243,6 +15886,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// The submitted frame still executes; RecreateSwapchainResources
|
||||
// drains it (and every frame slot) before destroying anything.
|
||||
RecreateSwapchainResources("vkQueuePresentKHR", presentResult);
|
||||
_presentedSequence = presentation.Sequence;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16445,7 +17089,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.CmdPipelineBarrier(
|
||||
_commandBuffer,
|
||||
shaderStage,
|
||||
shaderStage,
|
||||
PipelineStageFlags.AllCommandsBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanFormatConversionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.A2R10G10B10UnormPack32, true)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.A2B10G10R10UnormPack32, true)]
|
||||
[InlineData(Format.A2R10G10B10UnormPack32, Format.R8G8B8A8Unorm, true)]
|
||||
[InlineData(Format.A2B10G10R10UnormPack32, Format.R8G8B8A8Unorm, true)]
|
||||
public void RequiresRealFormatConversion_FlagsTheBitIncompatiblePair(
|
||||
Format from,
|
||||
Format to,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, VulkanVideoPresenter.RequiresRealFormatConversion(from, to));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.B8G8R8A8Unorm)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Srgb)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Unorm)]
|
||||
[InlineData(Format.A2R10G10B10UnormPack32, Format.A2B10G10R10UnormPack32)]
|
||||
[InlineData(Format.R16G16B16A16Sfloat, Format.R32G32Sfloat)]
|
||||
public void RequiresRealFormatConversion_LeavesEveryOtherPairAlone(Format from, Format to)
|
||||
{
|
||||
Assert.False(VulkanVideoPresenter.RequiresRealFormatConversion(from, to));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BitCastOfOpaqueBlackRgba8AsA2r10g10b10_ProducesTheObservedRed()
|
||||
{
|
||||
const uint opaqueBlackRgba8 = 0xFF000000u; // bytes 00 00 00 FF, little-endian
|
||||
|
||||
var alpha2Bit = (opaqueBlackRgba8 >> 30) & 0x3u;
|
||||
var red10Bit = (opaqueBlackRgba8 >> 20) & 0x3FFu;
|
||||
var green10Bit = (opaqueBlackRgba8 >> 10) & 0x3FFu;
|
||||
var blue10Bit = opaqueBlackRgba8 & 0x3FFu;
|
||||
|
||||
Assert.Equal(3u, alpha2Bit);
|
||||
Assert.Equal(1008u, red10Bit);
|
||||
Assert.Equal(0u, green10Bit);
|
||||
Assert.Equal(0u, blue10Bit);
|
||||
|
||||
var redAsFloat = red10Bit / 1023.0;
|
||||
Assert.True(
|
||||
Math.Abs(redAsFloat - 0.9853372434443793) < 0.0001,
|
||||
$"expected ~0.9853 (matches the red observed live), got {redAsFloat}");
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ public sealed class VulkanGuestImageAliasTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Format.R8Srgb, Format.R8Unorm)]
|
||||
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
||||
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
||||
Format existing,
|
||||
@@ -68,6 +67,15 @@ public sealed class VulkanGuestImageAliasTests
|
||||
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void R8SrgbAndR8UnormShareOneCompatibilityClass()
|
||||
{
|
||||
Assert.True(
|
||||
VulkanVideoPresenter.IsCompatibleGuestImageViewFormat(
|
||||
Format.R8Srgb,
|
||||
Format.R8Unorm));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AliasedPairStaysWithinOneCompatibilityClass()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user