mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-04 00:49:45 +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
|
? renderBudgetMs
|
||||||
: OperatingSystem.IsMacOS() ? 12L : 0L) *
|
: OperatingSystem.IsMacOS() ? 12L : 0L) *
|
||||||
System.Diagnostics.Stopwatch.Frequency / 1000L;
|
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
|
// 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.
|
// GPU fence before skipping the frame and returning to the event pump.
|
||||||
// Prevents the window freezing behind a slow-compute GPU backlog.
|
// 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"),
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WORK_COMPLETION"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
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)>
|
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
|
||||||
_tracedGuestImageSubmissions = [];
|
_tracedGuestImageSubmissions = [];
|
||||||
private static Thread? _thread;
|
private static Thread? _thread;
|
||||||
@@ -1511,6 +1534,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_guestImageWorkSequences[texture.Address] = workSequence;
|
_guestImageWorkSequences[texture.Address] = workSequence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_thread is null)
|
||||||
|
{
|
||||||
|
StartPresenterLocked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workSequence;
|
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)
|
private static void CompleteGuestWork(in PendingGuestWork pending)
|
||||||
{
|
{
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
|
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
|
||||||
@@ -3082,6 +3129,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
target is not null &&
|
target is not null &&
|
||||||
(state.TestEnable || state.WriteEnable || state.ClearEnable);
|
(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(
|
private readonly record struct Presentation(
|
||||||
byte[]? Pixels,
|
byte[]? Pixels,
|
||||||
uint Width,
|
uint Width,
|
||||||
@@ -3569,6 +3624,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public RenderPass RenderPass;
|
public RenderPass RenderPass;
|
||||||
public RenderPass InitialRenderPass;
|
public RenderPass InitialRenderPass;
|
||||||
public Framebuffer Framebuffer;
|
public Framebuffer Framebuffer;
|
||||||
|
public Dictionary<Format, ReinterpretedGuestImageViews> ReinterpretCache { get; } = new();
|
||||||
public Dictionary<GuestDepthKey, DepthFramebufferResource> DepthFramebuffers { get; } = new();
|
public Dictionary<GuestDepthKey, DepthFramebufferResource> DepthFramebuffers { get; } = new();
|
||||||
public bool Initialized;
|
public bool Initialized;
|
||||||
public bool InitialUploadPending;
|
public bool InitialUploadPending;
|
||||||
@@ -3577,6 +3633,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public bool SupportsStorageUsage;
|
public bool SupportsStorageUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct ReinterpretedGuestImageViews(
|
||||||
|
ImageView View,
|
||||||
|
ImageView[] MipViews,
|
||||||
|
RenderPass RenderPass,
|
||||||
|
RenderPass InitialRenderPass,
|
||||||
|
Framebuffer Framebuffer);
|
||||||
|
|
||||||
private sealed record PendingGuestSubmission(
|
private sealed record PendingGuestSubmission(
|
||||||
Fence Fence,
|
Fence Fence,
|
||||||
CommandBuffer CommandBuffer,
|
CommandBuffer CommandBuffer,
|
||||||
@@ -6059,6 +6122,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
_deferredGuestImageVersionDestroys.Dequeue();
|
_deferredGuestImageVersionDestroys.Dequeue();
|
||||||
DestroyGuestImage(imageEntry.Image);
|
DestroyGuestImage(imageEntry.Image);
|
||||||
|
FlipProgressTracker.RecordFlip(imageEntry.Image.FlipVersion);
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.flip_retired version={imageEntry.Image.FlipVersion} " +
|
$"vk.flip_retired version={imageEntry.Image.FlipVersion} " +
|
||||||
$"timeline={imageEntry.RetireTimeline} reason=presentation-dropped");
|
$"timeline={imageEntry.RetireTimeline} reason=presentation-dropped");
|
||||||
@@ -6084,6 +6148,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_frameGuestImageVersions[slot] = null;
|
_frameGuestImageVersions[slot] = null;
|
||||||
_capturedGuestFlipVersions.Remove(unsubmittedVersion.FlipVersion);
|
_capturedGuestFlipVersions.Remove(unsubmittedVersion.FlipVersion);
|
||||||
DestroyGuestImage(unsubmittedVersion);
|
DestroyGuestImage(unsubmittedVersion);
|
||||||
|
FlipProgressTracker.RecordFlip(unsubmittedVersion.FlipVersion);
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.flip_retired version={unsubmittedVersion.FlipVersion} " +
|
$"vk.flip_retired version={unsubmittedVersion.FlipVersion} " +
|
||||||
$"frame_slot={slot} reason=frame-not-submitted");
|
$"frame_slot={slot} reason=frame-not-submitted");
|
||||||
@@ -6117,6 +6182,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_frameGuestImageVersions[slot] = null;
|
_frameGuestImageVersions[slot] = null;
|
||||||
_capturedGuestFlipVersions.Remove(guestImageVersion.FlipVersion);
|
_capturedGuestFlipVersions.Remove(guestImageVersion.FlipVersion);
|
||||||
DestroyGuestImage(guestImageVersion);
|
DestroyGuestImage(guestImageVersion);
|
||||||
|
FlipProgressTracker.RecordFlip(guestImageVersion.FlipVersion);
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.flip_retired version={guestImageVersion.FlipVersion} " +
|
$"vk.flip_retired version={guestImageVersion.FlipVersion} " +
|
||||||
$"frame_slot={slot} timeline={_frameTimelines[slot]}");
|
$"frame_slot={slot} timeline={_frameTimelines[slot]}");
|
||||||
@@ -10157,7 +10223,26 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
var source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
var source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
||||||
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), 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 &&
|
if (!guestBuffer.Writable &&
|
||||||
(allocation.LastUseTimeline > _completedTimeline ||
|
(allocation.LastUseTimeline > _completedTimeline ||
|
||||||
@@ -11985,6 +12070,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var unreadablePages = 0;
|
var unreadablePages = 0;
|
||||||
var fallbackWrites = 0;
|
var fallbackWrites = 0;
|
||||||
var firstChangedOffset = -1;
|
var firstChangedOffset = -1;
|
||||||
|
var scanTicks = 0L;
|
||||||
|
var ioTicks = 0L;
|
||||||
allocation.DirtyRanges.RemoveAt(index);
|
allocation.DirtyRanges.RemoveAt(index);
|
||||||
|
|
||||||
// A writable descriptor only identifies a potential write
|
// 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.
|
// into millions of writes for alternating output patterns.
|
||||||
const int pageSize = 4096;
|
const int pageSize = 4096;
|
||||||
const int unreadableMergeGap = 16;
|
const int unreadableMergeGap = 16;
|
||||||
|
const int FragmentationRunThreshold = 64;
|
||||||
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||||
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
|
||||||
var pageRuns = new List<(int Start, int Length)>(64);
|
var pageRuns = new List<(int Start, int Length)>(64);
|
||||||
|
var coalescedPageRun = new List<(int Start, int Length)>(1);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (var pageStart = 0;
|
for (var pageStart = 0;
|
||||||
@@ -12022,66 +12111,126 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
|
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
|
||||||
mappedPageSource.CopyTo(mappedPage);
|
mappedPageSource.CopyTo(mappedPage);
|
||||||
pageRuns.Clear();
|
pageRuns.Clear();
|
||||||
var cursor = 0;
|
var scanStartTicks = _traceGlobalWritebackTiming
|
||||||
while (cursor < pageLength)
|
? 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 &&
|
var blockEnd = Math.Min(blockStart + coarseBlockSize, pageLength);
|
||||||
mappedPage[cursor] == shadowPage[cursor])
|
coarseBlockCount++;
|
||||||
|
if (!mappedPage.Slice(blockStart, blockEnd - blockStart).SequenceEqual(
|
||||||
|
shadowPage.Slice(blockStart, blockEnd - blockStart)))
|
||||||
{
|
{
|
||||||
cursor++;
|
coarseDiffBlockCount++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cursor == pageLength)
|
if (coarseBlockCount > 0 && coarseDiffBlockCount * 4 >= coarseBlockCount)
|
||||||
{
|
{
|
||||||
break;
|
pageRuns.Add((pageStart, pageLength));
|
||||||
}
|
|
||||||
|
|
||||||
var runStart = cursor;
|
|
||||||
while (cursor < pageLength &&
|
|
||||||
mappedPage[cursor] != shadowPage[cursor])
|
|
||||||
{
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var runLength = cursor - runStart;
|
|
||||||
pageRuns.Add((pageStart + runStart, runLength));
|
|
||||||
changedRuns++;
|
changedRuns++;
|
||||||
changedBytes += (ulong)runLength;
|
changedBytes += (ulong)pageLength;
|
||||||
if (firstChangedOffset < 0)
|
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)
|
if (pageRuns.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
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++;
|
changedPages++;
|
||||||
var livePage = livePageBuffer.AsSpan(0, pageLength);
|
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(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
livePage.Slice(run.Start - pageStart, run.Length));
|
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(
|
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
|
||||||
shadowBytes.Slice(run.Start, run.Length));
|
shadowBytes.Slice(run.Start, run.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
writtenPages++;
|
writtenPages++;
|
||||||
writtenRuns += pageRuns.Count;
|
writtenRuns += runsToWrite.Count;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var run in pageRuns)
|
foreach (var run in runsToWrite)
|
||||||
{
|
{
|
||||||
failedRuns++;
|
failedRuns++;
|
||||||
MarkGuestBufferDirty(
|
MarkGuestBufferDirty(
|
||||||
@@ -12221,10 +12370,63 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"probe_nonzero={nonzero}/{probe.Length} " +
|
$"probe_nonzero={nonzero}/{probe.Length} " +
|
||||||
$"changed_head={Convert.ToHexString(head)}");
|
$"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(
|
private void RecordChunkedComputeDispatch(
|
||||||
CommandBuffer commandBuffer,
|
CommandBuffer commandBuffer,
|
||||||
VulkanComputeGuestDispatch work,
|
VulkanComputeGuestDispatch work,
|
||||||
@@ -13540,6 +13742,51 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return existing;
|
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)
|
if (_traceGuestImageEvents)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -14292,6 +14539,358 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
mipLevel >= 32
|
mipLevel >= 32
|
||||||
? 1
|
? 1
|
||||||
: Math.Max(dimension >> (int)mipLevel, 1u);
|
: 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)
|
private void DestroyGuestImage(GuestImageResource resource)
|
||||||
{
|
{
|
||||||
@@ -14310,6 +14909,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
resource.FormatViews.Clear();
|
resource.FormatViews.Clear();
|
||||||
|
|
||||||
|
foreach (var cached in resource.ReinterpretCache.Values)
|
||||||
|
{
|
||||||
|
DestroyReinterpretedGuestImageViews(cached);
|
||||||
|
}
|
||||||
|
resource.ReinterpretCache.Clear();
|
||||||
|
|
||||||
if (resource.Framebuffer.Handle != 0)
|
if (resource.Framebuffer.Handle != 0)
|
||||||
{
|
{
|
||||||
_vk.DestroyFramebuffer(_device, resource.Framebuffer, null);
|
_vk.DestroyFramebuffer(_device, resource.Framebuffer, null);
|
||||||
@@ -14517,9 +15122,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
format switch
|
format switch
|
||||||
{
|
{
|
||||||
Format.R8Unorm or
|
Format.R8Unorm or
|
||||||
|
Format.R8SNorm or
|
||||||
|
Format.R8Srgb or
|
||||||
Format.R8Uint or
|
Format.R8Uint or
|
||||||
Format.R8Sint or
|
Format.R8Sint => 8,
|
||||||
Format.R8SNorm => 8,
|
|
||||||
// Every single-channel 16-bit format shares this class, not just
|
// Every single-channel 16-bit format shares this class, not just
|
||||||
// the float one. Omitting the rest made GetVulkanImageByteCount
|
// the float one. Omitting the rest made GetVulkanImageByteCount
|
||||||
// return zero for them, and a zero expected size rejects the
|
// return zero for them, and a zero expected size rejects the
|
||||||
@@ -14533,26 +15139,36 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Format.R16Sint or
|
Format.R16Sint or
|
||||||
Format.R8G8Unorm or
|
Format.R8G8Unorm or
|
||||||
Format.R8G8SNorm or
|
Format.R8G8SNorm or
|
||||||
|
Format.R8G8Srgb or
|
||||||
Format.R8G8Uint or
|
Format.R8G8Uint or
|
||||||
Format.R8G8Sint => 16,
|
Format.R8G8Sint => 16,
|
||||||
Format.R32Uint or
|
Format.R32Uint or
|
||||||
Format.R32Sint or
|
Format.R32Sint or
|
||||||
Format.R32Sfloat or
|
Format.R32Sfloat or
|
||||||
Format.R16G16Unorm or
|
Format.R16G16Unorm or
|
||||||
|
Format.R16G16SNorm or
|
||||||
Format.R16G16Uint or
|
Format.R16G16Uint or
|
||||||
Format.R16G16Sint or
|
Format.R16G16Sint or
|
||||||
Format.R16G16Sfloat or
|
Format.R16G16Sfloat or
|
||||||
Format.R8G8B8A8Unorm or
|
Format.R8G8B8A8Unorm or
|
||||||
|
Format.R8G8B8A8SNorm or
|
||||||
Format.R8G8B8A8Srgb or
|
Format.R8G8B8A8Srgb or
|
||||||
Format.R8G8B8A8Uint or
|
Format.R8G8B8A8Uint or
|
||||||
Format.R8G8B8A8Sint or
|
Format.R8G8B8A8Sint or
|
||||||
|
Format.B8G8R8A8Unorm or
|
||||||
|
Format.B8G8R8A8SNorm or
|
||||||
|
Format.B8G8R8A8Srgb or
|
||||||
|
Format.A8B8G8R8UnormPack32 or
|
||||||
|
Format.A8B8G8R8SrgbPack32 or
|
||||||
Format.A2R10G10B10UnormPack32 or
|
Format.A2R10G10B10UnormPack32 or
|
||||||
Format.A2B10G10R10UnormPack32 or
|
Format.A2B10G10R10UnormPack32 or
|
||||||
Format.B10G11R11UfloatPack32 => 32,
|
Format.B10G11R11UfloatPack32 or
|
||||||
|
Format.E5B9G9R9UfloatPack32 => 32,
|
||||||
Format.R32G32Uint or
|
Format.R32G32Uint or
|
||||||
Format.R32G32Sint or
|
Format.R32G32Sint or
|
||||||
Format.R32G32Sfloat or
|
Format.R32G32Sfloat or
|
||||||
Format.R16G16B16A16Unorm or
|
Format.R16G16B16A16Unorm or
|
||||||
|
Format.R16G16B16A16SNorm or
|
||||||
Format.R16G16B16A16Uint or
|
Format.R16G16B16A16Uint or
|
||||||
Format.R16G16B16A16Sint or
|
Format.R16G16B16A16Sint or
|
||||||
Format.R16G16B16A16Sfloat => 64,
|
Format.R16G16B16A16Sfloat => 64,
|
||||||
@@ -14660,10 +15276,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
var completedWork = 0;
|
var completedWork = 0;
|
||||||
HashSet<string>? deferredOrderedQueues = null;
|
HashSet<string>? deferredOrderedQueues = null;
|
||||||
var workBudgetTicks = _renderWorkBudgetTicks;
|
var drainStartTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
var renderWorkDeadline = workBudgetTicks > 0
|
var renderWorkDeadline = _renderWorkBudgetTicks > 0
|
||||||
? System.Diagnostics.Stopwatch.GetTimestamp() + workBudgetTicks
|
? drainStartTicks + _renderWorkBudgetTicks
|
||||||
: long.MaxValue;
|
: long.MaxValue;
|
||||||
|
var followupDeadline = _guestWorkFollowupBudgetTicks > 0
|
||||||
|
? drainStartTicks + _guestWorkFollowupBudgetTicks
|
||||||
|
: long.MinValue;
|
||||||
var workLimit = _maxGuestWorkPerRender;
|
var workLimit = _maxGuestWorkPerRender;
|
||||||
// Prefer ordered sync / flip heads while the queue is elevated so
|
// Prefer ordered sync / flip heads while the queue is elevated so
|
||||||
// label wakeups are not starved behind fat compute/draw items on
|
// label wakeups are not starved behind fat compute/draw items on
|
||||||
@@ -14701,7 +15320,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
if (!tookGuestWork)
|
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(
|
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} " +
|
$"queued_ms={(System.Diagnostics.Stopwatch.GetTimestamp() - pendingGuestWork.EnqueuedTicks) * 1000.0 / System.Diagnostics.Stopwatch.Frequency:F3} " +
|
||||||
work.GetType().Name);
|
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
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -14963,6 +15602,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
presentation.TranslatedDraw is null &&
|
presentation.TranslatedDraw is null &&
|
||||||
presentation.GuestImageAddress == 0)
|
presentation.GuestImageAddress == 0)
|
||||||
{
|
{
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14979,6 +15619,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_extent.Height);
|
_extent.Height);
|
||||||
if ((ulong)pixels.Length > _stagingSize)
|
if ((ulong)pixels.Length > _stagingSize)
|
||||||
{
|
{
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15018,6 +15659,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
DestroyGuestImage(presentedGuestImage);
|
DestroyGuestImage(presentedGuestImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ownsPresentedGuestImageVersion)
|
if (ownsPresentedGuestImageVersion)
|
||||||
@@ -15105,6 +15747,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ownsPresentedGuestImageVersion,
|
ownsPresentedGuestImageVersion,
|
||||||
presentedGuestImage);
|
presentedGuestImage);
|
||||||
|
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15243,6 +15886,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// The submitted frame still executes; RecreateSwapchainResources
|
// The submitted frame still executes; RecreateSwapchainResources
|
||||||
// drains it (and every frame slot) before destroying anything.
|
// drains it (and every frame slot) before destroying anything.
|
||||||
RecreateSwapchainResources("vkQueuePresentKHR", presentResult);
|
RecreateSwapchainResources("vkQueuePresentKHR", presentResult);
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16445,7 +17089,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
shaderStage,
|
shaderStage,
|
||||||
shaderStage,
|
PipelineStageFlags.AllCommandsBit,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
null,
|
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]
|
[Theory]
|
||||||
[InlineData(Format.R8Srgb, Format.R8Unorm)]
|
|
||||||
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
||||||
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
||||||
Format existing,
|
Format existing,
|
||||||
@@ -68,6 +67,15 @@ public sealed class VulkanGuestImageAliasTests
|
|||||||
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void R8SrgbAndR8UnormShareOneCompatibilityClass()
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
VulkanVideoPresenter.IsCompatibleGuestImageViewFormat(
|
||||||
|
Format.R8Srgb,
|
||||||
|
Format.R8Unorm));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AliasedPairStaysWithinOneCompatibilityClass()
|
public void AliasedPairStaysWithinOneCompatibilityClass()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user